mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
test(perf): launch J2 without SQL counting and stamp mock requests in sub-ms
- runLaunchJourney takes the launch instrumentation; only J1 turns on the main-process counters and IPTVNATOR_PERF_COUNT_SQL, so J2's click is not measured under the hook that wraps every SQLite statement. The flags are built in journey-launch-environment.ts, which the SQL opt-in guard now expects, and a launch record without main counters is rejected. - The mock ledger stamps arrivals with performance.timeOrigin + performance.now(), the same sub-millisecond epoch as the renderer's click, so a request later in the click's millisecond is not counted before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
058df6f08e
commit
a28b26b550
10 files changed
+103
-22
No files matched your search
@@ -32,6 +32,10 @@ import {
|
||||
assertJourneyMainCounters,
|
||||
readJourneyMainCounters,
|
||||
} from '../performance/journey-main-counters';
|
||||
import {
|
||||
journeyLaunchEnvironment,
|
||||
type JourneyLaunchInstrumentation,
|
||||
} from '../performance/journey-launch-environment';
|
||||
import {
|
||||
createLaunchJourneyProbeOptions,
|
||||
installJourneyRendererProbe,
|
||||
@@ -129,6 +133,7 @@ export async function measureLaunchJourney(
|
||||
const { launch } = await runLaunchJourney(
|
||||
templateDirectory,
|
||||
timeoutMs,
|
||||
{ mainCounters: true },
|
||||
async () => undefined
|
||||
);
|
||||
return launch;
|
||||
@@ -142,10 +147,13 @@ export async function measureLaunchJourney(
|
||||
* therefore in place before the renderer runs any script, and the probe,
|
||||
* capture and gate records still prove it. `continueJourney` runs in the
|
||||
* same process after J1's counters are final, before the app is closed.
|
||||
* Without `instrumentation.mainCounters` the main-process counters and SQL
|
||||
* counting stay off and `launch.mainCounters` is null.
|
||||
*/
|
||||
export async function runLaunchJourney<T>(
|
||||
templateDirectory: string,
|
||||
timeoutMs: number,
|
||||
instrumentation: JourneyLaunchInstrumentation,
|
||||
continueJourney: (session: LaunchJourneySession) => Promise<T>
|
||||
): Promise<{
|
||||
readonly continuation: T;
|
||||
@@ -156,17 +164,9 @@ export async function runLaunchJourney<T>(
|
||||
);
|
||||
try {
|
||||
await cp(templateDirectory, dataDirectory, { recursive: true });
|
||||
// IPTVNATOR_PERF_CAPTURE turns on the main-process counters and
|
||||
// their read handler, IPTVNATOR_PERF_COUNT_SQL the SQL statement
|
||||
// count behind main.sqlStatementsBeforeReadyToShow; only this journey
|
||||
// sets it. See journey-main-counters.ts.
|
||||
const env = buildElectronLaunchEnvironment(
|
||||
dataDirectory,
|
||||
launchOptions({
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
IPTVNATOR_PERF_COUNT_SQL: '1',
|
||||
IPTVNATOR_TRACE_IPC: '1',
|
||||
})
|
||||
launchOptions(journeyLaunchEnvironment(instrumentation))
|
||||
);
|
||||
const args = buildElectronLaunchArgs([
|
||||
'-r',
|
||||
@@ -229,13 +229,15 @@ export async function runLaunchJourney<T>(
|
||||
10_000
|
||||
);
|
||||
// Read after the probe finished, so both frozen counters exist.
|
||||
const mainCounters = assertJourneyMainCounters(
|
||||
await readJourneyMainCounters(
|
||||
electronApp,
|
||||
JOURNEY_RENDERER_GATE_KEY
|
||||
),
|
||||
gate
|
||||
);
|
||||
const mainCounters = instrumentation.mainCounters
|
||||
? assertJourneyMainCounters(
|
||||
await readJourneyMainCounters(
|
||||
electronApp,
|
||||
JOURNEY_RENDERER_GATE_KEY
|
||||
),
|
||||
gate
|
||||
)
|
||||
: null;
|
||||
if (ipc.installedEpochMs > renderer.installed.epochMs) {
|
||||
throw new Error('journey-main-ipc-capture-installed-late');
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ test('J2 open a source', async () => {
|
||||
const { continuation, launch } = await runLaunchJourney(
|
||||
templateDirectory,
|
||||
JOURNEY_ITERATION_TIMEOUT_MS,
|
||||
// J2 does not read J1's main-process counters, so their
|
||||
// SQL instrumentation stays off during the click.
|
||||
{ mainCounters: false },
|
||||
(session) =>
|
||||
measureOpenSourceJourney(
|
||||
session,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { journeyLaunchEnvironment } from './journey-launch-environment';
|
||||
|
||||
test('J1 launches with the main-process counters and SQL counting', () => {
|
||||
assert.deepEqual(journeyLaunchEnvironment({ mainCounters: true }), {
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
IPTVNATOR_PERF_COUNT_SQL: '1',
|
||||
IPTVNATOR_TRACE_IPC: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('a journey that does not read the main counters launches with the IPC trace only', () => {
|
||||
assert.deepEqual(journeyLaunchEnvironment({ mainCounters: false }), {
|
||||
IPTVNATOR_TRACE_IPC: '1',
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Instrumentation flags a journey launch sets on the Electron process.
|
||||
*
|
||||
* Every journey needs the renderer-API trace (`IPTVNATOR_TRACE_IPC`) for its
|
||||
* IPC counters. Only J1 records the main-process counters:
|
||||
* `IPTVNATOR_PERF_CAPTURE` turns on the counters and their read handler, and
|
||||
* `IPTVNATOR_PERF_COUNT_SQL` wraps every main-thread and worker SQLite
|
||||
* statement to count it (see journey-main-counters.ts). A journey that
|
||||
* continues from the launch without reading them (J2) leaves both off, so
|
||||
* its latency and workload are not measured under that extra
|
||||
* instrumentation.
|
||||
*/
|
||||
export interface JourneyLaunchInstrumentation {
|
||||
readonly mainCounters: boolean;
|
||||
}
|
||||
|
||||
export function journeyLaunchEnvironment(
|
||||
instrumentation: JourneyLaunchInstrumentation
|
||||
): Record<string, string> {
|
||||
return {
|
||||
...(instrumentation.mainCounters
|
||||
? { IPTVNATOR_PERF_CAPTURE: '1', IPTVNATOR_PERF_COUNT_SQL: '1' }
|
||||
: {}),
|
||||
IPTVNATOR_TRACE_IPC: '1',
|
||||
};
|
||||
}
|
||||
@@ -143,5 +143,10 @@ test('only the launch journey opts into SQL statement counting', () => {
|
||||
)
|
||||
)
|
||||
.map((file) => relative(sourceRoot, join(sourceRoot, file)));
|
||||
assert.deepEqual(optedIn, [join('journeys', 'launch-journey-app.ts')]);
|
||||
// The journey launch builds its flags in one place, and only the
|
||||
// launch journey asks for the main counters there (J2 launches with
|
||||
// `mainCounters: false`; see journey-launch-environment.spec.ts).
|
||||
assert.deepEqual(optedIn, [
|
||||
join('performance', 'journey-launch-environment.ts'),
|
||||
]);
|
||||
});
|
||||
@@ -87,6 +87,7 @@ test('forwards every request to the mock and records it from a mark', async () =
|
||||
]);
|
||||
const mark = ledger.mark();
|
||||
assert.equal(mark, 1);
|
||||
const beforeSecond = performance.timeOrigin + performance.now();
|
||||
const second = await fetch(
|
||||
`${ledger.origin}/player_api.php?username=u&password=p&action=get_account_info`
|
||||
);
|
||||
@@ -103,6 +104,10 @@ test('forwards every request to the mock and records it from a mark', async () =
|
||||
);
|
||||
assert.equal(since[0]?.method, 'GET');
|
||||
assert.equal(since[0]?.sequence, 1);
|
||||
// Stamped on the same high-resolution epoch as the renderer's click.
|
||||
const stamped = since[0]?.epochMs ?? 0;
|
||||
assert.ok(stamped >= beforeSecond);
|
||||
assert.ok(stamped <= performance.timeOrigin + performance.now());
|
||||
assert.ok(!JSON.stringify(ledger.since(0)).includes('password'));
|
||||
} finally {
|
||||
await ledger.close();
|
||||
|
||||
@@ -23,6 +23,12 @@ import type { AddressInfo } from 'node:net';
|
||||
* never stored.
|
||||
*/
|
||||
export interface JourneyMockRequest {
|
||||
/**
|
||||
* Arrival as a sub-millisecond epoch (`performance.timeOrigin +
|
||||
* performance.now()`), the same form as the renderer's click stamp. With
|
||||
* `Date.now()` a request later in the click's millisecond would compare
|
||||
* as earlier than the fractional click and be counted before it.
|
||||
*/
|
||||
readonly epochMs: number;
|
||||
readonly method: string;
|
||||
readonly route: string;
|
||||
@@ -94,7 +100,7 @@ export async function startJourneyMockRequestLedger(
|
||||
active -= 1;
|
||||
});
|
||||
requests.push({
|
||||
epochMs: Date.now(),
|
||||
epochMs: performance.timeOrigin + performance.now(),
|
||||
method: incoming.method ?? 'GET',
|
||||
route: describeJourneyMockRoute(incoming.url),
|
||||
sequence: requests.length,
|
||||
|
||||
@@ -155,6 +155,13 @@ test('rejects measurements whose clocks or probes are inconsistent', () => {
|
||||
}),
|
||||
/incomplete-probe/
|
||||
);
|
||||
// A launch that ran without the main-process counters (as J2's do) is
|
||||
// not a J1 measurement.
|
||||
assert.throws(
|
||||
() =>
|
||||
toLaunchIterationRecord(0, false, { ...base, mainCounters: null }),
|
||||
/main-counters-missing/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
toLaunchIterationRecord(0, false, { ...base, spawnEpochMs: 2_700 }),
|
||||
|
||||
@@ -45,7 +45,8 @@ export interface LaunchJourneyMeasurement {
|
||||
readonly electronVersion: string;
|
||||
readonly gate: JourneyRendererGateState;
|
||||
readonly ipc: JourneyMainIpcCaptureState;
|
||||
readonly mainCounters: JourneyMainCountersState;
|
||||
/** Null when the launch ran without the main-process counters (J2). */
|
||||
readonly mainCounters: JourneyMainCountersState | null;
|
||||
readonly pid: number;
|
||||
readonly renderer: JourneyRendererProbeState;
|
||||
readonly spawnEpochMs: number;
|
||||
@@ -61,6 +62,9 @@ export function toLaunchIterationRecord(
|
||||
measurement: LaunchJourneyMeasurement
|
||||
): JourneyIterationRecord {
|
||||
const { ipc, mainCounters, renderer, spawnEpochMs } = measurement;
|
||||
if (mainCounters === null) {
|
||||
throw new Error('launch-journey-record-main-counters-missing');
|
||||
}
|
||||
if (renderer.terminal === null || renderer.navigation === null) {
|
||||
throw new Error('launch-journey-record-incomplete-probe');
|
||||
}
|
||||
|
||||
@@ -120,8 +120,9 @@ registers the `performance:read-counters` IPC handler. Without the flag
|
||||
nothing is counted, no listener is attached and the handler does not exist;
|
||||
the preload never exposes the channel. SQL statements are counted only with
|
||||
`IPTVNATOR_PERF_COUNT_SQL=1` as well, because the hook wraps every statement
|
||||
execution: the journey sets both, while the M3U, refresh and Xtream
|
||||
benchmarks run with the capture flag alone and keep measuring unwrapped
|
||||
execution: the launch journey sets both (the flags are built in
|
||||
`journey-launch-environment.ts`), while J2's launches and the M3U, refresh
|
||||
and Xtream benchmarks do not set the SQL flag and keep measuring unwrapped
|
||||
statements. A harness test fails if any other source sets the SQL flag. After the renderer probe completes,
|
||||
`journey-main-counters.ts` calls the handler through `electronApp.evaluate`
|
||||
and the gate's tap.
|
||||
@@ -247,7 +248,11 @@ running app to `measureOpenSourceJourney` in
|
||||
`src/journeys/open-source-journey-app.ts`. The click therefore happens after
|
||||
J1's terminal condition and its counters are final, and the two journeys never
|
||||
overlap. One warm-up and five measured iterations, as for J1; the J1 numbers
|
||||
of these launches are not reported again.
|
||||
of these launches are not reported again. J2 does not read J1's
|
||||
main-process counters, so its launches run without `IPTVNATOR_PERF_CAPTURE`
|
||||
and `IPTVNATOR_PERF_COUNT_SQL` (`runLaunchJourney` with
|
||||
`mainCounters: false`). The click is not measured under the SQL hook that
|
||||
wraps every statement.
|
||||
|
||||
**Start.** The click on the dashboard card of the Xtream portal
|
||||
(`dashboard-recent-sources-rail-card` with the portal's name; the probe also
|
||||
|
||||
Reference in new issue
Block a user