348 lines
14 KiB
Rust
348 lines
14 KiB
Rust
//! Manual timing through the real Document and App paths. No disk writes,
|
|
//! executor polling, layout, GPU work or first-paint measurement occurs here.
|
|
//! Register as a test child of app::source_rename to use its actual worker helpers.
|
|
use super::*;
|
|
use crate::source_workspace::Workspace;
|
|
use oa_core::source_project::SourceBundle;
|
|
use std::{
|
|
collections::BTreeMap,
|
|
path::PathBuf,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
#[cfg(debug_assertions)]
|
|
#[path = "source_rename_layout_timing_tests.rs"]
|
|
mod layout_timing_tests;
|
|
|
|
const OLD: &str = "oa_restore_work";
|
|
const NEW: &str = "oa_restore_pressure";
|
|
const DECLARATIONS: &str = "oa/activities/activity_oa_restore_work.arcadia";
|
|
const ORIGIN: &str = "traceability.arcadia";
|
|
|
|
struct Fixture {
|
|
bundle: SourceBundle,
|
|
project: Project,
|
|
expected_bundle: SourceBundle,
|
|
expected_project: Project,
|
|
target: Id,
|
|
cursor: text_editor::Cursor,
|
|
}
|
|
|
|
impl Fixture {
|
|
fn load() -> Self {
|
|
// The optional entry selects a parent-pinned copy of this exact fixture,
|
|
// not an arbitrary performance generator or a mutable GUI destination.
|
|
let entry = std::env::var_os("ARCADIA_RENAME_TIMING_ENTRY")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../examples/release1/project.arcadia")
|
|
});
|
|
let loaded = oa_core::source_files::load_project(&entry, LIMITS).unwrap();
|
|
let bundle = loaded.reachable_bundle();
|
|
assert_eq!(
|
|
bundle.files.len(),
|
|
30,
|
|
"use the preserved release1 walkthrough"
|
|
);
|
|
let document = Document::from_bundle(bundle.clone(), LIMITS).unwrap();
|
|
assert_eq!(
|
|
document.bundle(),
|
|
&bundle,
|
|
"fixture must need no stabilization/migration"
|
|
);
|
|
assert!(!document.dirty());
|
|
let project = document.project().clone();
|
|
let target = Id::parse_str("1b91f8a7-0bdb-514e-b1cd-955658e09927").unwrap();
|
|
assert_eq!(project.elements[&target].alias, OLD);
|
|
assert!(!project.identities().iter().any(|(_, alias)| *alias == NEW));
|
|
let mut expected_project = project.clone();
|
|
expected_project.elements.get_mut(&target).unwrap().alias = NEW.into();
|
|
|
|
// Independent literal source oracle, authored before running the rename.
|
|
// Do not replace the alias substring in filenames or the trace's alias.
|
|
let mut expected_bundle = bundle.clone();
|
|
let declarations = expected_bundle.files.get_mut(DECLARATIONS).unwrap();
|
|
assert_eq!(
|
|
declarations.matches("oa_activity oa_restore_work ").count(),
|
|
1
|
|
);
|
|
assert_eq!(declarations.matches("parent @oa_restore_work;").count(), 4);
|
|
*declarations = declarations
|
|
.replace(
|
|
"oa_activity oa_restore_work ",
|
|
"oa_activity oa_restore_pressure ",
|
|
)
|
|
.replace("parent @oa_restore_work;", "parent @oa_restore_pressure;");
|
|
let traces = expected_bundle.files.get_mut(ORIGIN).unwrap();
|
|
assert_eq!(traces.matches("oa @oa_restore_work;").count(), 1);
|
|
*traces = traces.replace("oa @oa_restore_work;", "oa @oa_restore_pressure;");
|
|
let source = &bundle.files[ORIGIN];
|
|
let start = source.rfind("@oa_restore_work;").unwrap() + 1;
|
|
let position = |offset| {
|
|
let prefix = &source[..offset];
|
|
text_editor::Position {
|
|
line: prefix.bytes().filter(|byte| *byte == b'\n').count(),
|
|
column: prefix
|
|
.rsplit_once('\n')
|
|
.map_or(prefix.len(), |(_, tail)| tail.len()),
|
|
}
|
|
};
|
|
let cursor = text_editor::Cursor {
|
|
position: position(start),
|
|
selection: Some(position(start + OLD.len())),
|
|
};
|
|
assert!(nav::exact_cursor(source, cursor));
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({
|
|
"record": "fixture", "entry": entry, "reachable_files": bundle.files.len(),
|
|
"source_bytes": bundle.files.values().map(String::len).sum::<usize>(),
|
|
"identities": project.identities().len(), "active_file": ORIGIN,
|
|
"active_file_bytes": source.len(), "old": OLD, "new": NEW,
|
|
"warmups": 1, "measured_trials": 5, "debug_assertions": cfg!(debug_assertions),
|
|
"scope": "real worker helpers, guarded Document commit, App sync and Confirm update; no task polling or rendering"
|
|
})
|
|
);
|
|
Self {
|
|
bundle,
|
|
project,
|
|
expected_bundle,
|
|
expected_project,
|
|
target,
|
|
cursor,
|
|
}
|
|
}
|
|
|
|
fn app(&self) -> App {
|
|
let mut app = App::with_project(Project::empty());
|
|
app.document = Document::from_bundle(self.bundle.clone(), LIMITS).unwrap();
|
|
app.source_workspace = Workspace::new(app.document.bundle(), true);
|
|
app.source_workspace.active = ORIGIN.into();
|
|
app.selected = app.project().id;
|
|
app.page = Page::Text;
|
|
app.sync();
|
|
app.source_editor.move_to(self.cursor);
|
|
assert_eq!(app.project(), &self.project);
|
|
assert_eq!(app.document.bundle(), &self.bundle);
|
|
assert_eq!(app.document.revision(), 0);
|
|
assert!(!app.document.dirty() && !app.document.can_undo() && !app.document.can_redo());
|
|
assert!(app.rename_admission().is_ok());
|
|
app
|
|
}
|
|
|
|
fn check_candidate(&self, prepared: &PreparedAliasRename) {
|
|
assert!(prepared.changed());
|
|
assert_eq!(prepared.before().bundle, self.bundle);
|
|
assert_eq!(prepared.before().decoded.project, self.project);
|
|
assert_eq!(prepared.after().bundle, self.expected_bundle);
|
|
assert_eq!(prepared.after().decoded.project, self.expected_project);
|
|
}
|
|
|
|
fn check_adopted(&self, app: &mut App, candidate_address: *const CompiledSourceProject) {
|
|
assert!(
|
|
std::ptr::eq(app.document.compiled(), candidate_address),
|
|
"adopt exact candidate allocation"
|
|
);
|
|
assert_eq!(app.project(), &self.expected_project);
|
|
assert_eq!(app.document.bundle(), &self.expected_bundle);
|
|
assert_eq!(app.source_workspace.bundle(), self.expected_bundle);
|
|
assert_eq!(app.source_editor.text(), self.expected_bundle.files[ORIGIN]);
|
|
assert_eq!(app.document.revision(), 1);
|
|
assert!(app.document.dirty() && app.document.can_undo() && !app.document.can_redo());
|
|
assert!(app.document.undo());
|
|
assert_eq!(app.project(), &self.project);
|
|
assert_eq!(app.document.bundle(), &self.bundle);
|
|
assert!(
|
|
!app.document.dirty(),
|
|
"live known saved baseline remains authoritative"
|
|
);
|
|
assert!(
|
|
!app.document.can_undo(),
|
|
"exactly one preceding model transaction"
|
|
);
|
|
assert!(app.document.redo());
|
|
assert_eq!(app.project(), &self.expected_project);
|
|
assert_eq!(app.document.bundle(), &self.expected_bundle);
|
|
assert!(std::ptr::eq(app.document.compiled(), candidate_address));
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Samples(BTreeMap<&'static str, Vec<Duration>>);
|
|
|
|
impl Samples {
|
|
fn time<T>(&mut self, phase: &'static str, trial: usize, action: impl FnOnce() -> T) -> T {
|
|
let started = Instant::now();
|
|
let result = action();
|
|
let elapsed = started.elapsed();
|
|
// Printing, assertions and dropping the returned value are outside this
|
|
// interval. Full Confirm still includes production's own local drops.
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({
|
|
"record": "timing", "phase": phase, "trial": trial,
|
|
"warmup": trial == 0, "elapsed_ns": elapsed.as_nanos().to_string(),
|
|
"elapsed_ms": elapsed.as_secs_f64() * 1000.0
|
|
})
|
|
);
|
|
if trial != 0 {
|
|
self.0.entry(phase).or_default().push(elapsed);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn report(mut self) {
|
|
for (phase, values) in &mut self.0 {
|
|
assert_eq!(values.len(), 5);
|
|
values.sort();
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({
|
|
"record": "summary", "phase": phase, "samples": values.len(),
|
|
"min_ms": values[0].as_secs_f64() * 1000.0,
|
|
"median_ms": values[2].as_secs_f64() * 1000.0,
|
|
"max_ms": values[4].as_secs_f64() * 1000.0
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resolve(app: &App, context: &Context) -> Target {
|
|
let snapshot = app
|
|
.with_source_input(|input| query::admit_owned(input, query::Limits::default()))
|
|
.unwrap()
|
|
.capture()
|
|
.unwrap();
|
|
let offset = nav::cursor_offset(app.source_workspace.text(), context.begin.cursor).unwrap();
|
|
resolve_target(&snapshot, context, offset).unwrap()
|
|
}
|
|
|
|
fn active_job(app: &App) -> Job {
|
|
let form = app.source_rename.form.as_ref().unwrap();
|
|
let id = form.pending.unwrap();
|
|
assert_eq!(app.source_rename.in_flight, Some(id));
|
|
Job {
|
|
id,
|
|
invocation: form.invocation,
|
|
generation: form.generation,
|
|
context: form.context.clone(),
|
|
}
|
|
}
|
|
|
|
fn separate_phases(fixture: &Fixture, samples: &mut Samples, trial: usize) {
|
|
let mut app = fixture.app();
|
|
let target = resolve(&app, &app.rename_context());
|
|
assert_eq!(target.id, fixture.target);
|
|
let request = samples
|
|
.time("document_request_mint", trial, || {
|
|
app.document.alias_rename_request(target.id, NEW.into())
|
|
})
|
|
.unwrap();
|
|
let prepared = samples
|
|
.time("worker_document_prepare", trial, || request.prepare())
|
|
.unwrap();
|
|
fixture.check_candidate(&prepared);
|
|
let approval = samples
|
|
.time("worker_preview_and_origin", trial, || {
|
|
prepare_approval(prepared, &target, ORIGIN)
|
|
})
|
|
.unwrap();
|
|
assert_eq!(approval.preview.files().len(), 2);
|
|
assert_eq!(approval.preview.changed_line_count(), 6);
|
|
let candidate_address = approval.prepared.after() as *const CompiledSourceProject;
|
|
// Same-destination save completion is a legitimate live update after Preview.
|
|
app.document.mark_saved(app.document.snapshot());
|
|
let changed = samples
|
|
.time("document_guarded_commit", trial, || {
|
|
app.document.commit_alias_rename(&approval.prepared)
|
|
})
|
|
.unwrap();
|
|
assert!(changed);
|
|
samples.time("app_sync", trial, || app.sync());
|
|
fixture.check_adopted(&mut app, candidate_address);
|
|
}
|
|
|
|
fn whole_confirm(fixture: &Fixture, samples: &mut Samples, trial: usize) {
|
|
let mut app = fixture.app();
|
|
let begin = app.source_rename_message();
|
|
// These Tasks are deliberately not polled: deterministic worker helpers
|
|
// supply the same results through the real readiness messages below.
|
|
drop(app.update(begin));
|
|
let target_job = active_job(&app);
|
|
let target = Arc::new(resolve(&app, &target_job.context));
|
|
assert_eq!(target.id, fixture.target);
|
|
drop(app.update(Message::SourceRenameReady(Completion {
|
|
job: target_job,
|
|
payload: Payload::Target(Ok(target.clone())),
|
|
})));
|
|
let invocation = app.source_rename.form.as_ref().unwrap().invocation;
|
|
drop(app.update(Message::SourceRename(Event::Input(invocation, NEW.into()))));
|
|
let generation = app.source_rename.form.as_ref().unwrap().generation;
|
|
drop(app.update(Message::SourceRename(Event::Preview(
|
|
invocation, generation,
|
|
))));
|
|
let preview_job = active_job(&app);
|
|
let prepared = app
|
|
.document
|
|
.alias_rename_request(target.id, NEW.into())
|
|
.unwrap()
|
|
.prepare()
|
|
.unwrap();
|
|
fixture.check_candidate(&prepared);
|
|
let approval = Arc::new(prepare_approval(prepared, &target, ORIGIN).unwrap());
|
|
drop(target);
|
|
let candidate_address = approval.prepared.after() as *const CompiledSourceProject;
|
|
let expected_cursor = approval.cursor;
|
|
let approval_id = approval.id;
|
|
drop(app.update(Message::SourceRenameReady(Completion {
|
|
job: preview_job,
|
|
payload: Payload::Preview(Ok(approval)),
|
|
})));
|
|
assert_eq!(
|
|
app.source_rename
|
|
.form
|
|
.as_ref()
|
|
.unwrap()
|
|
.approval
|
|
.as_ref()
|
|
.unwrap()
|
|
.id,
|
|
approval_id
|
|
);
|
|
app.document.mark_saved(app.document.snapshot());
|
|
let confirm = Message::SourceRename(Event::Confirm(invocation, generation, approval_id));
|
|
let task = samples.time("app_confirm_update", trial, || app.update(confirm));
|
|
samples.time("confirm_returned_task_drop_without_polling", trial, || {
|
|
drop(task)
|
|
});
|
|
assert!(!app.source_rename.open());
|
|
assert!(app.source_rename.in_flight.is_none());
|
|
assert!(app.source_rename_undo_message().is_some());
|
|
assert_eq!(app.source_editor.cursor(), expected_cursor);
|
|
fixture.check_adopted(&mut app, candidate_address);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "manual Windows dev/debug rename latency trial; run alone with --ignored --nocapture --test-threads=1"]
|
|
fn accepted_release1_prepare_commit_sync_and_confirm_timing() {
|
|
let fixture = Fixture::load();
|
|
let mut samples = Samples::default();
|
|
for trial in 0..=5 {
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({ "record": "order", "trial": trial,
|
|
"warmup": trial == 0, "separate_phases_first": trial % 2 == 0 })
|
|
);
|
|
if trial % 2 == 0 {
|
|
separate_phases(&fixture, &mut samples, trial);
|
|
whole_confirm(&fixture, &mut samples, trial);
|
|
} else {
|
|
whole_confirm(&fixture, &mut samples, trial);
|
|
separate_phases(&fixture, &mut samples, trial);
|
|
}
|
|
}
|
|
samples.report();
|
|
}
|