Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8d287d5
Keep the open book in BookService's cache beside the latest
imnasnainaec Oct 19, 2026
83b2841
Pass the analyses to the bench's concordance step
imnasnainaec Oct 19, 2026
b0976fe
Read the whole text in the host with a form-in-text index (#439)
imnasnainaec Oct 19, 2026
519345f
Correct what the whole-text read holds, and match forms as analyses do
imnasnainaec Oct 19, 2026
bc19707
Say onAnalysisChanged covers every book the whole-text read has read
imnasnainaec Oct 19, 2026
b4a12d7
Keep the latest outline's book open, and report no edit once a read has
alex-rawlings-yyc Oct 19, 2026
decc5b0
Report an edit to the open book the whole-text read could not watch
alex-rawlings-yyc Oct 20, 2026
331b2e3
Record the 2e checkpoint run, and name each result for what measured it
alex-rawlings-yyc Oct 20, 2026
bc338f1
Run Prettier
alex-rawlings-yyc Oct 20, 2026
96c8ab2
Head the bench's form-index timing for what it measures
alex-rawlings-yyc Oct 20, 2026
f1381eb
List a text's books again after reporting they could not be listed
alex-rawlings-yyc Oct 20, 2026
9d953e2
Report an edit to the open book once, whichever watch hears it first
alex-rawlings-yyc Oct 20, 2026
6227810
Report a reread edit only once its read has filed the book
alex-rawlings-yyc Oct 20, 2026
5de4622
Reword comments that read one way and mean another
imnasnainaec Oct 20, 2026
5675828
Sort imports
imnasnainaec Oct 20, 2026
0049419
Drop redundant tests and assertions on stand-ins
imnasnainaec Oct 20, 2026
488ad4e
Test missing forms on their own and build the form-index book with ma…
imnasnainaec Oct 20, 2026
fee4a10
Keep the word stand-in's offsets true and say FormIndex.has takes a n…
imnasnainaec Oct 21, 2026
9a54a8e
Read a book the whole-text read could not subscribe to again on the n…
imnasnainaec Oct 21, 2026
cf3fba9
Read books the whole-text read could not subscribe to again a few at …
imnasnainaec Oct 21, 2026
cafa970
Say READ_CONCURRENCY bounds each pass of the whole-text read
imnasnainaec Oct 21, 2026
4c7a76b
Get the USJ provider with the book list, and reject the status when l…
imnasnainaec Oct 21, 2026
da7bb2b
Log a failed text read status, and drop the bookCount 0 note
imnasnainaec Oct 21, 2026
e6c4f75
Report no edit for a book that fails to read again, and tag has's for…
imnasnainaec Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions e2e-tests/tests/perf/app-perf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TIERS, type TierSpec } from '../../../perf/dataset/tiers';
import { provenance, writeResults } from '../../../perf/results';
import { addSavedProject, readDraft, writeDraft } from '../../../perf/storage';
import { readCaptureMeta } from '../../../perf/usj-cache';
import type { TextReadStatus } from '../../../src/types/text-read-status';
import { expect, test } from '../../fixtures/cdp.fixture';
import { waitForAppAndInterlinearizerReady } from '../../fixtures/helpers';
import {
Expand Down Expand Up @@ -81,6 +82,34 @@ async function waitForAutosave(frame: Frame): Promise<Measure[]> {
return measures;
}

/**
* Reads the host's whole-text read status for the source, starting the read when it has none.
*
* @returns The status, or `undefined` when the command answers with something other than a string.
*/
async function textReadStatus(page: Page, projectId: string): Promise<TextReadStatus | undefined> {
const json = await sendCommand(page, 'interlinearizer.getTextReadStatus', projectId);
return typeof json === 'string' ? JSON.parse(json) : undefined;
}

/**
* Starts the host's read of the source's whole text, and waits until it has read every book.
*
* @throws If any book failed to read, since the read's timing would then leave it out.
*/
async function readWholeText(page: Page, projectId: string): Promise<void> {
await expect
.poll(
async () => {
const status = await textReadStatus(page, projectId);
return status !== undefined && status.booksRead === status.bookCount;
},
{ timeout: 300_000, intervals: [250] },
)
.toBe(true);
expect((await textReadStatus(page, projectId))?.failedBookIds).toEqual([]);
}

/** Waits for any autosave an edit or Save started to land. */
async function settleAutosave(): Promise<void> {
if (unsavedFrame) await waitForAutosave(unsavedFrame);
Expand Down Expand Up @@ -433,6 +462,39 @@ async function measureTier(
);
}
result.concordance = concordance.summary();

// Whole-text read in the host, in a fresh view each run, since the host keeps the read until
// the view closes.
const textRead = new Samples();
for (let run = 0; run < RUNS; run += 1) {
const isLast = run === RUNS - 1;
// eslint-disable-next-line no-await-in-loop
await closeSettledView(page);
// eslint-disable-next-line no-await-in-loop
const { frame: view } = await openView(page, projectId, tier.viewBook);
// eslint-disable-next-line no-await-in-loop
await waitForMeasure(view, 'reanchor', tier.viewBook);
// eslint-disable-next-line no-await-in-loop
if (isLast) await recordMemory('beforeTextRead');
// eslint-disable-next-line no-await-in-loop
await takeHostEntries(page);
// eslint-disable-next-line no-await-in-loop
await readWholeText(page, projectId);
// eslint-disable-next-line no-await-in-loop
const entries = await takeHostEntries(page);
entries
.filter(({ name }) => name === 'readText')
.forEach(({ duration }) => textRead.addValue('text-read', duration));
textRead.addValue(
'text-read-tokenize',
entries
.filter(({ name }) => name === 'readText:tokenize')
.reduce((total, { duration }) => total + duration, 0),
);
// eslint-disable-next-line no-await-in-loop
if (isLast) await recordMemory('afterTextRead');
}
result.textRead = textRead.summary();
}

test('measure the app', async ({ mainPage }) => {
Expand Down
2 changes: 1 addition & 1 deletion perf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Measures the extension rather than tests it: the data layer's pure functions in

`PERF_TIERS` (comma-separated tier names) and `PERF_RUNS` (10 by default) narrow a run. The `large` tier's draft exceeds one websocket message, so only the Node benchmarks can measure it.

Results land in `perf/results/` as JSON, each recording the machine, its load average, and both commits.
Results land in `perf/results/` as JSON, each recording the machine, its load average, and both commits. Before committing one, suffix its name with the issue it was measured for and why, such as `-407-baseline`; `perf:summary` reads the time from the file, not its name.

## Dataset shape

Expand Down
31 changes: 30 additions & 1 deletion perf/bench/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
deriveConcordanceRows,
indexBook,
} from '../../src/utils/concordance';
import { FormIndex, formsOfBook } from '../../src/utils/form-index';
import { collatorForTag } from '../../src/utils/language-tags';
import { reanchorAnalysisToBook } from '../../src/utils/reanchor-analysis';
import { MAX_UNDO_STEPS } from '../../src/utils/undo-history';
Expand Down Expand Up @@ -143,12 +144,35 @@ function benchTier(tier: TierSpec, allBooks: () => Book[]): Record<string, unkno
const entries = buildConcordanceEntries(books.map(indexBook), collator);
deriveConcordanceRows(
entries,
approvedAnalysisByToken(analysis.tokenAnalysisLinks),
approvedAnalysisByToken(
analysis.tokenAnalysisLinks,
new Map(analysis.tokenAnalyses.map((ta) => [ta.id, ta])),
),
viewBookCode,
);
}),
);

const indexForms = () => {
const index = new FormIndex();
books.forEach((book) => index.setBook(book.bookRef, formsOfBook(book)));
return index;
};
time('text.formIndex', measure(opts, noSetup, indexForms));
// The host's form index and the WebView's read must agree on which catalog forms are missing.
const index = indexForms();
const textForms = new Set(books.flatMap((book) => [...indexBook(book).occurrencesByForm.keys()]));
const catalogForms = selectCatalogRows(
{ analysis, analysisLanguage: language },
viewBookCode,
).map((row) => row.form);
const byWebViewRead = catalogForms.filter((form) => !textForms.has(form));
const byHostIndex = catalogForms.filter((form) => !index.has(form));
const disagreeing = catalogForms.filter((form) => textForms.has(form) !== index.has(form)).length;
console.log(
` forms not in the text: ${byWebViewRead.length} by the WebView's read, ${byHostIndex.length} by the host's index, ${disagreeing} disagreeing`,
);

// The undo history holds a snapshot of the analysis before each of its steps.
const undoBytes = retainedBytes(() => {
let state: AnalysisState = { analysis, analysisLanguage: language };
Expand All @@ -166,6 +190,11 @@ function benchTier(tier: TierSpec, allBooks: () => Book[]): Record<string, unkno
viewBook: viewBookCode,
dataset: { counts: manifest.counts, bytes: manifest.bytes },
timings,
formsNotInText: {
byWebViewRead: byWebViewRead.length,
byHostIndex: byHostIndex.length,
disagreeing,
},
memory: { parsedDraftBytes: analysisBytes, undoHistoryBytes: undoBytes },
};
}
Expand Down
Loading
Loading