Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
229 changes: 105 additions & 124 deletions framework/runners/record-reorder.runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
type SeedCacheInfo,
} from "../seed-cache";
import { withPerfTraceStep } from "../trace-collector";
import { PerfRunDiagnosticError } from "../types";
import type {
MetricThreshold,
PerfCase,
Expand All @@ -33,16 +32,15 @@ import type {
RecordUndoRedoBaseCaseConfig,
} from "../types";
import {
buildRecordWindowId,
undoRedoMixed20Fields,
withRecordWindowId,
type Measurement,
} from "./record-undo-redo.shared";

type Measurement<T> = {
name: string;
durationMs: number;
result: T;
};
import {
runRecordMutationLifecycle,
seedRecordMutationLifecycle,
type RecordMutationLifecycleSpec,
} from "./record-mutation-lifecycle";

type ReorderField = RecordReorderCaseConfig["fields"][number] & {
id: string;
Expand Down Expand Up @@ -1083,139 +1081,122 @@ const buildResult = ({
},
});

export const runRecordReorderCase = async (
// The single measured operation, run inside the driver's record window:
// trace-wrapped block reorder -> routing assertion -> post-reorder position
// verification, all bundled into one reorder measurement whose duration is the
// primary metric.
const runReorderMeasuredOperation = async (
perfCase: PerfCase,
context: PerfRunContext,
): Promise<PerfRunResult> => {
const config = perfCase.config as RecordReorderCaseConfig;
const baseId = globalThis.testConfig.baseId;
const tableName = `${config.tableNamePrefix}-${Date.now()}`;
const windowId = buildRecordWindowId(context, perfCase);
let prepareMeasurement: Measurement<ReorderFixture> | undefined;
let seedReadyMeasurement:
| Measurement<Awaited<ReturnType<typeof assertSeedReady>>>
| undefined;

try {
prepareMeasurement = await measureAsync("prepare", () =>
prepareFixture(baseId, tableName, config, perfCase),
);
const fixture = prepareMeasurement.result;
seedReadyMeasurement = await measureAsync("seedReady", () =>
assertSeedReady(fixture, config),
);
let reorderMeasurement: Measurement<ReorderOperationResult> | undefined;
config: RecordReorderCaseConfig,
fixture: ReorderFixture,
): Promise<Measurement<ReorderOperationResult>> => {
const requestMeasurement = await measureAsync(config.threshold.metric, () =>
executeReorder(context, perfCase, fixture, config),
);
let reorderMeasurement: Measurement<ReorderOperationResult> = {
...requestMeasurement,
result: {
...requestMeasurement.result,
requestMs: requestMeasurement.durationMs,
routing: assertEngineRouting(
context,
requestMeasurement.result.responseHeaders,
{
operation: "updateRecordOrders",
},
),
},
};
const verification = await verifyReorder(fixture, config);
reorderMeasurement = {
...reorderMeasurement,
result: {
...reorderMeasurement.result,
...verification,
},
};
return reorderMeasurement;
};

// The measured reorder moves the reusable seed rows, so a shared (non-isolated)
// execute DB is restored to the original order inside the same record window —
// or the table dropped if restore fails. The non-reusable case just drops the
// table. Isolated CI execute DBs are discarded after the job.
const cleanupReorderFixture = async ({
baseId,
fixture,
config,
windowId,
}: {
baseId: string;
fixture: ReorderFixture | undefined;
config: RecordReorderCaseConfig;
windowId: string;
}) => {
if (fixture?.tableId && fixture.reusableSeed && !isExecuteDbIsolated()) {
try {
await withRecordWindowId(windowId, async () => {
const requestMeasurement = await measureAsync(
config.threshold.metric,
() => executeReorder(context, perfCase, fixture, config),
);
reorderMeasurement = {
...requestMeasurement,
result: {
...requestMeasurement.result,
requestMs: requestMeasurement.durationMs,
routing: assertEngineRouting(
context,
requestMeasurement.result.responseHeaders,
{
operation: "updateRecordOrders",
},
),
},
};
const verification = await verifyReorder(fixture, config);
reorderMeasurement = {
...reorderMeasurement,
result: {
...reorderMeasurement.result,
...verification,
},
};
});
await withRecordWindowId(windowId, () =>
restoreOriginalOrder(fixture, config),
);
} catch (error) {
throw new PerfRunDiagnosticError(
error instanceof Error ? error.message : String(error),
buildResult({
config,
windowId,
fixture,
prepareMeasurement,
seedReadyMeasurement,
reorderMeasurement,
error,
}),
console.warn(
`Failed to restore cached record reorder seed ${fixture.tableId}; deleting it`,
error,
);
}

return buildResult({
config,
windowId,
fixture,
prepareMeasurement,
seedReadyMeasurement,
reorderMeasurement,
});
} finally {
const fixture = prepareMeasurement?.result;
if (fixture?.tableId && fixture.reusableSeed && !isExecuteDbIsolated()) {
try {
await withRecordWindowId(windowId, () =>
restoreOriginalOrder(fixture, config),
);
} catch (error) {
await permanentDeleteTable(baseId, fixture.tableId);
} catch (cleanupError) {
console.warn(
`Failed to restore cached record reorder seed ${fixture.tableId}; deleting it`,
error,
`Failed to cleanup perf table ${fixture.tableId}`,
cleanupError,
);
try {
await permanentDeleteTable(baseId, fixture.tableId);
} catch (cleanupError) {
console.warn(
`Failed to cleanup perf table ${fixture.tableId}`,
cleanupError,
);
}
}
} else if (
fixture?.tableId &&
!fixture.reusableSeed &&
!isExecuteDbIsolated()
) {
try {
await permanentDeleteTable(baseId, fixture.tableId);
} catch (error) {
console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error);
}
}
} else if (
fixture?.tableId &&
!fixture.reusableSeed &&
!isExecuteDbIsolated()
) {
try {
await permanentDeleteTable(baseId, fixture.tableId);
} catch (error) {
console.warn(`Failed to cleanup perf table ${fixture.tableId}`, error);
}
}
};

export const seedRecordReorderCase = async (
perfCase: PerfCase,
_context: PerfRunContext,
): Promise<PerfRunResult> => {
const config = perfCase.config as RecordReorderCaseConfig;
const baseId = globalThis.testConfig.baseId;
const tableName = `${config.tableNamePrefix}-seed-${Date.now()}`;
const prepareMeasurement = await measureAsync("prepare", () =>
const recordReorderLifecycleSpec: RecordMutationLifecycleSpec<
RecordReorderCaseConfig,
ReorderFixture,
Awaited<ReturnType<typeof assertSeedReady>>,
ReorderOperationResult
> = {
// Group the reorder write under one record window id (mirrors the legacy
// runner; the same window scopes the restore in cleanup).
useRecordWindow: true,
prepareFixture: ({ baseId, tableName, config, perfCase }) =>
prepareFixture(baseId, tableName, config, perfCase),
);
const seedReadyMeasurement = await measureAsync("seedReady", () =>
assertSeedReady(prepareMeasurement.result, config),
);

return buildResult({
config,
windowId: `seed-${_context.runId}-${perfCase.id}`,
fixture: prepareMeasurement.result,
prepareMeasurement,
seedReadyMeasurement,
});
assertSeedReady: ({ fixture, config }) => assertSeedReady(fixture, config),
runMeasuredOperation: ({ perfCase, context, config, fixture }) =>
runReorderMeasuredOperation(perfCase, context, config, fixture),
buildResult: ({ primaryMeasurement, ...rest }) =>
buildResult({ ...rest, reorderMeasurement: primaryMeasurement }),
cleanup: cleanupReorderFixture,
};

export const runRecordReorderCase = async (
perfCase: PerfCase,
context: PerfRunContext,
): Promise<PerfRunResult> =>
runRecordMutationLifecycle(perfCase, context, recordReorderLifecycleSpec);

export const seedRecordReorderCase = async (
perfCase: PerfCase,
context: PerfRunContext,
): Promise<PerfRunResult> =>
seedRecordMutationLifecycle(perfCase, context, recordReorderLifecycleSpec);

export const recordReorderMixed10kBaseConfig = {
baseId: "seed-base" as const,
rowCount: 10_000,
Expand Down
21 changes: 21 additions & 0 deletions scripts/diff-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ const GENERATED_ID_KEYS = new Set([
"duplicatedRecordIds",
"sourceRecordId",
"duplicatedRecordId",
// Generated record ids in the record-reorder verification evidence. Each run
// seeds a fresh table, so the moved block's record ids and the
// first/anchor record id differ between two runs of unchanged code (confirmed
// by the record-reorder baseline A vs B diff). The semantic reorder proof —
// checkedPositions[].expectedOriginalRowNumber / viewOffset and
// verifiedSamples[].expected — stays visible; only the opaque id strings are
// masked, like the existing `recordId`.
"firstRecordId",
"anchorRecordId",
"movedRecordIds",
]);

const GENERATED_NAME_KEYS = new Set(["foreignTableName", "tableName"]);
Expand Down Expand Up @@ -185,6 +195,17 @@ const shouldMaskKey = (path, key) => {
return true;
}

// record-reorder serializes its raw per-batch seed timings as an array under
// details.prepare. Like every other *Ms / maxSeedBatchMs duration, these vary
// run-to-run on unchanged code (confirmed by the record-reorder baseline A vs
// B diff); the batch count stays visible elsewhere.
if (
pathEquals(path, ["details", "prepare"]) &&
key === "seedBatchDurations"
) {
return true;
}

return false;
};

Expand Down
36 changes: 18 additions & 18 deletions tasks/runner-migration-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,27 @@ that uses it, because migrating a runner means re-verifying all of its cases.

Status as of 2026-06-19 on `main`.

**Migrated: 14 / 35 runner kinds · 16 / 55 cases.**
**Migrated: 15 / 35 runner kinds · 17 / 55 cases.**

## Migrated (✅ on the driver)

| Runner kind | Driver / where | Cases | Verified |
| ----------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------- | --------------------- |
| csv-import | `csv-import-lifecycle.ts` | 3 csv-import cases | ✅ v1+v2 pass (local) |
| field-delete | `field-delete-lifecycle.ts` | field-delete/mixed-10k-delete-19-fields | ✅ v1+v2 pass (local) |
| record-delete | `record-replay-lifecycle.ts` (no setup) | record-delete/delete-1k | ✅ v1+v2 pass (local) |
| record-delete-link | `table-link-lifecycle.ts` (single fixture) | record-delete/link-trash-1k | ✅ v1+v2 pass (local) |
| record-undo | `record-replay-lifecycle.ts` (delete setup) | record-undo/delete-1k | ✅ v1+v2 pass (local) |
| record-redo | `record-replay-lifecycle.ts` (delete+undo setup) | record-redo/delete-1k | ✅ v1+v2 pass (local) |
| table-delete | `table-lifecycle.ts` (sampled archive + restore-back) | table-delete/10k-20f | ✅ v1+v2 pass (local) |
| table-restore | `table-lifecycle.ts` (archive setup + sampled restore) | table-restore/10k-20f | ✅ v1+v2 pass (local) |
| table-delete-link | `table-link-lifecycle.ts` (sampled foreign-table delete) | table-delete/10k-20f-link-detach | ✅ v1+v2 pass (local) |
| table-restore-link | `table-link-lifecycle.ts` (sampled table restore) | table-restore/10k-20f-link-1k | ✅ v1+v2 pass (local) |
| selection-duplicate | `record-duplicate-lifecycle.ts` (stream block duplicate) | record-duplicate/grid-block-duplicate-1k | ✅ v1+v2 pass (local) |
| record-duplicate-single | `record-duplicate-lifecycle.ts` (sequential single dup) | record-duplicate/single-record-sequential-100 | ✅ v1+v2 pass (local) |
| record-update | `record-mutation-lifecycle.ts` (bulk update over seeded rows, record window + restore-or-delete) | record-update/mixed-1k-20fields-bulk-update | ✅ v1+v2 pass (local) |
| record-create | `record-mutation-lifecycle.ts` (bulk insert into empty seed, no window + delete-created-or-drop) | record-create/mixed-1k-20fields-bulk-create | ✅ v1+v2 pass (local) |
| Runner kind | Driver / where | Cases | Verified |
| ----------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------- | --------------------- |
| csv-import | `csv-import-lifecycle.ts` | 3 csv-import cases | ✅ v1+v2 pass (local) |
| field-delete | `field-delete-lifecycle.ts` | field-delete/mixed-10k-delete-19-fields | ✅ v1+v2 pass (local) |
| record-delete | `record-replay-lifecycle.ts` (no setup) | record-delete/delete-1k | ✅ v1+v2 pass (local) |
| record-delete-link | `table-link-lifecycle.ts` (single fixture) | record-delete/link-trash-1k | ✅ v1+v2 pass (local) |
| record-undo | `record-replay-lifecycle.ts` (delete setup) | record-undo/delete-1k | ✅ v1+v2 pass (local) |
| record-redo | `record-replay-lifecycle.ts` (delete+undo setup) | record-redo/delete-1k | ✅ v1+v2 pass (local) |
| table-delete | `table-lifecycle.ts` (sampled archive + restore-back) | table-delete/10k-20f | ✅ v1+v2 pass (local) |
| table-restore | `table-lifecycle.ts` (archive setup + sampled restore) | table-restore/10k-20f | ✅ v1+v2 pass (local) |
| table-delete-link | `table-link-lifecycle.ts` (sampled foreign-table delete) | table-delete/10k-20f-link-detach | ✅ v1+v2 pass (local) |
| table-restore-link | `table-link-lifecycle.ts` (sampled table restore) | table-restore/10k-20f-link-1k | ✅ v1+v2 pass (local) |
| selection-duplicate | `record-duplicate-lifecycle.ts` (stream block duplicate) | record-duplicate/grid-block-duplicate-1k | ✅ v1+v2 pass (local) |
| record-duplicate-single | `record-duplicate-lifecycle.ts` (sequential single dup) | record-duplicate/single-record-sequential-100 | ✅ v1+v2 pass (local) |
| record-update | `record-mutation-lifecycle.ts` (bulk update over seeded rows, record window + restore-or-delete) | record-update/mixed-1k-20fields-bulk-update | ✅ v1+v2 pass (local) |
| record-create | `record-mutation-lifecycle.ts` (bulk insert into empty seed, no window + delete-created-or-drop) | record-create/mixed-1k-20fields-bulk-create | ✅ v1+v2 pass (local) |
| record-reorder | `record-mutation-lifecycle.ts` (block reorder over seeded rows, record window + restore-order-or-drop) | record-reorder/10k-move-last-1k-to-front | ✅ v1+v2 pass (local) |

## Not migrated (⬜ legacy `*.runner.ts`)

Expand All @@ -47,7 +48,6 @@ Status as of 2026-06-19 on `main`.
| lookup-search-index | 2 | search/search-index-off-10k-20search-fields, search/search-index-on-10k-20search-fields |
| record-paste | 4 | record-paste/flat-10k-4fields-copy-paste, record-paste/flat-10k-20fields-copy-paste, record-paste/mixed-10k-20fields-complex-copy-paste, selection-paste/10k-expand-rows-and-fields-stream |
| record-read | 2 | record-read/10k-50fields-10x1k-pages, record-read/10k-50fields-filter-sort-groupby-overhead |
| record-reorder | 1 | record-reorder/10k-move-last-1k-to-front |
| record-update-attachment | 1 | record-update/attachment-insert-100 |
| record-update-link | 1 | record-update/1k-link-cells-bulk-update |
| selection-clear | 1 | selection-clear/flat-1k-20fields-cell-clear-stream |
Expand Down