From 19d949fd132c59070ce084e56dbabd998a10ed35 Mon Sep 17 00:00:00 2001 From: karl Date: Tue, 13 Oct 2026 09:56:34 +0200 Subject: [PATCH] Measure semantic activation refresh independently from download and idle time --- apps/desktop/src/main.ts | 41 +++++++++---- apps/desktop/src/search-performance.ts | 15 +++-- .../desktop/tests/search-performance.test.mjs | 27 +++++++++ docs/EARLY-TEXT-SETUP.md | 11 +++- docs/LINUX-DESKTOP-VALIDATION.md | 60 +++++++++++++++++++ docs/PERFORMANCE.md | 8 ++- 6 files changed, 140 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c2f4874..74ff8cc 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -30,7 +30,11 @@ import { spanDepth, briefMs, } from "./search-performance"; -import type { MeasuredResponse, SearchLane } from "./search-performance"; +import type { + MeasuredResponse, + SearchLane, + SearchRefreshTrigger, +} from "./search-performance"; import { displayPath } from "./path-display"; import { ResultSelection } from "./selection-state"; import { @@ -449,7 +453,7 @@ async function refreshSemanticStatus() { canSearchSemantically() && latestSearch?.id === requestId ) - submitSearch(latestSearch, "hybrid"); + refreshIndexedSearch("semantic_activation"); if (working || (!status.ready && !error)) queueSemanticRefresh(); } catch (error) { element("semantic-status").textContent = @@ -1088,7 +1092,22 @@ function submitSearch(request: SearchRequest, lane: SearchLane) { (lane === "lexical" ? lexicalLane : hybridLane).submit(request); } -function refreshIndexedSearch() { +function searchMeasurementContext() { + return { + model: semanticStatus?.worker_info?.model ?? null, + device: semanticStatus?.worker_info?.device ?? null, + runtime: semanticStatus?.worker_info?.runtime ?? null, + threads: semanticStatus?.worker_info?.threads ?? null, + dimensions: semanticStatus?.worker_info?.dimensions ?? null, + semantic_ready: canSearchSemantically(), + semantic_generation: semanticStatus?.generation ?? null, + indexed_chunks: semanticStatus?.chunks ?? null, + }; +} + +function refreshIndexedSearch( + trigger: SearchRefreshTrigger = "filesystem_refresh", +) { const previous = latestSearch; if ( !previous || @@ -1106,7 +1125,11 @@ function refreshIndexedSearch() { ...previous, id: ++requestId, started: refreshStarted, - measurement: previous.measurement.freshForRefresh(refreshStarted), + measurement: previous.measurement.freshForRefresh( + refreshStarted, + trigger, + searchMeasurementContext(), + ), }; latestSearch = refreshed; submitSearch(refreshed, "lexical"); @@ -1188,15 +1211,7 @@ async function search(inputAt = performance.now()) { inputAt, performance.now(), element("performance-toggle").checked, - { - model: semanticStatus?.worker_info?.model ?? null, - device: semanticStatus?.worker_info?.device ?? null, - runtime: semanticStatus?.worker_info?.runtime ?? null, - threads: semanticStatus?.worker_info?.threads ?? null, - dimensions: semanticStatus?.worker_info?.dimensions ?? null, - semantic_ready: canSearchSemantically(), - indexed_chunks: semanticStatus?.chunks ?? null, - }, + searchMeasurementContext(), ), }; latestSearch = request; diff --git a/apps/desktop/src/search-performance.ts b/apps/desktop/src/search-performance.ts index 7fc3219..cc11fed 100644 --- a/apps/desktop/src/search-performance.ts +++ b/apps/desktop/src/search-performance.ts @@ -17,6 +17,7 @@ export interface MeasuredResponse { metrics: NativeMetrics; } export type SearchLane = "lexical" | "hybrid"; +export type SearchRefreshTrigger = "filesystem_refresh" | "semantic_activation"; export interface AttemptStart { lane: SearchLane; startedAt: number; @@ -44,6 +45,7 @@ interface InputTimingSummary { export class SearchMeasurement { private originalInput: Readonly | null = null; + private trigger: "user_search" | SearchRefreshTrigger = "user_search"; debug: boolean; inputAt: number; scheduledAt: number; @@ -74,9 +76,14 @@ export class SearchMeasurement { this.debug = debug; this.context = { ...context }; } - /** Filesystem updates are independent requests, not elapsed user idle time. */ - freshForRefresh(now: number): SearchMeasurement { - const refreshed = new SearchMeasurement(now, now, this.debug, this.context); + /** Background updates are independent requests, not elapsed user idle time. */ + freshForRefresh( + now: number, + trigger: SearchRefreshTrigger = "filesystem_refresh", + context = this.context, + ): SearchMeasurement { + const refreshed = new SearchMeasurement(now, now, this.debug, context); + refreshed.trigger = trigger; // Preserve one fixed-size numeric snapshot, never an ancestor trace/history. // Pending responses on the old request cannot rewrite this snapshot. refreshed.originalInput = Object.freeze({ @@ -165,7 +172,7 @@ export class SearchMeasurement { export() { return { version: 1, - trigger: this.originalInput ? "filesystem_refresh" : "user_search", + trigger: this.trigger, original_input: this.originalInput ? { ...this.originalInput } : null, context: this.context, clock_note: diff --git a/apps/desktop/tests/search-performance.test.mjs b/apps/desktop/tests/search-performance.test.mjs index 47ed733..13cf584 100644 --- a/apps/desktop/tests/search-performance.test.mjs +++ b/apps/desktop/tests/search-performance.test.mjs @@ -161,3 +161,30 @@ test('repeated refreshes retain a bounded immutable numeric origin without repla assert.equal(origin.first_results_ms, 72); assert.equal(origin.hybrid_final_results_ms, null); }); + +test('semantic activation excludes download time and records the promoted context separately', () => { + const original = new SearchMeasurement(100, 100, true, {indexed_chunks:3}); + original.submitted('hybrid', 220); + finish(original, 'hybrid', 220, 230, native, false); + original.displayed('hybrid', 230, 238, 3); + const context = {indexed_chunks:4, semantic_ready:true}; + const promoted = original.freshForRefresh(144000, 'semantic_activation', context); + context.indexed_chunks = 999; + promoted.submitted('lexical', 144000); + finish(promoted, 'lexical', 144001, 144004, native, false); + promoted.displayed('lexical', 144004, 144005, 1); + promoted.submitted('hybrid', 144120); + finish(promoted, 'hybrid', 144120, 144128, native, false); + promoted.displayed('hybrid', 144128, 144138, 4); + const report = promoted.export(); + assert.equal(report.trigger, 'semantic_activation'); + assert.equal(report.first_results_ms, 5); + assert.equal(report.hybrid_final_results_ms, 138); + assert.equal(report.context.indexed_chunks, 4); + assert.equal(report.original_input.hybrid_final_results_ms, 138); + assert.equal(report.attempts.length, 2); + assert.equal(original.export().context.indexed_chunks, 3); + const next = promoted.freshForRefresh(300000); + assert.equal(next.export().trigger, 'filesystem_refresh'); + assert.deepEqual(next.export().original_input, report.original_input); +}); diff --git a/docs/EARLY-TEXT-SETUP.md b/docs/EARLY-TEXT-SETUP.md index c89dc64..27bd61d 100644 --- a/docs/EARLY-TEXT-SETUP.md +++ b/docs/EARLY-TEXT-SETUP.md @@ -61,9 +61,14 @@ passed **51**, and both strict all-target Clippy checks, formatting and the frontend production build passed. The actual Google integration above was run separately; ignored entries in the default suite are not execution evidence. -Native UI interaction during a genuine delayed HTTPS media download, failure, -retry and promotion remains to be executed. The prepared Linux QA plan uses only -an owned loopback CONNECT proxy and isolated app environment; it does not change +Native Linux UI interaction during a genuine delayed HTTPS media download, +failure, retry and full promotion passed on `9aae3f8`, retaining the query worker +and explicit selection. Its actual first unseen query reported 84 ms native and +293 ms through the DOM update on four fixture files. A later cached debug request +is a separate sample. The test exposed a semantic-activation clock defect; +the frontend now starts an independent refresh measurement, but a focused native +retest remains pending. See [native evidence](LINUX-DESKTOP-VALIDATION.md). +The owned loopback CONNECT proxy and isolated app environment do not change the product's pinned download URL or verification. Native Windows/macOS, GPU, large libraries and actual first-input measurements remain open. Document indexing still needs to finish a publishable generation; earlier model readiness does not diff --git a/docs/LINUX-DESKTOP-VALIDATION.md b/docs/LINUX-DESKTOP-VALIDATION.md index 46c8b6f..5ac832f 100644 --- a/docs/LINUX-DESKTOP-VALIDATION.md +++ b/docs/LINUX-DESKTOP-VALIDATION.md @@ -272,3 +272,63 @@ All owned test and benchmark handles ended successfully. Subsequent process inspection found no surviving Google workers, Cargo or Rust compiler processes. Native desktop packaging with this new worker, actual GPU, macOS and representative performance remain open. + +## Genuine first-use media download: `9aae3f8` + +Native embedded Release build passed in 2m 52s, using matching +`index-STH19o-O.js`. Executable SHA-256: +`229312e05629555956c659a83d56fa3dcc9e8830736a062c9208ce704ce11fa6`; +frontend SHA-256: +`dc3ee20bc7bb3eda347c142ee9fab3e10b8d17037402d21c5cc6de1a5f0dd967`. +The isolated resource override used the normal Linux query-role worker +`b52d50a1e676bd4bf64124daff1c4ce83d86a8bde01e4eaf09838cbb24a4c28f`. + +The fresh owned `desktop-qa-earlytext` directory isolated XDG cache/config/data, +settings, index, decoded scratch and model cache. Initially only the verified +309,855,456-byte backbone was present, hardlinked from the verified native cache. +A fixture contained three document/code files and a small BMP. The app's +HTTPS_PROXY/HTTP_PROXY pointed only its own process environment to an owned +loopback CONNECT proxy at `127.0.0.1:18943`, throttled to 4 MiB/s. The proxy +forwarded opaque TLS to real Hugging Face/CDN HTTPS; there was no TLS interception, +production URL override or global network change. + +Actual native UI evidence: + +- While the genuine projector download was incomplete (202,664,870-byte partial, + no installed projector), generation 1 contained three text chunks. The UI + reported text search ready and media preparation in progress. The first + actual remembered input found the supplier passage: one basic observation of + **84 ms native / 293 ms displayed**. This is not cold startup or warm p95. +- Closing owned proxy tunnels and rejecting further CONNECT attempts caused + three download attempts to fail and removed the partial file. Generation 1 + and query worker PID 397 remained usable. Expanded model details showed + media setup failure, text availability and an actual Retry model setup button. +- Restoring proxy forwarding and clicking that UI button restarted the genuine + pinned download. A separate cached debug rerun during retry measured + **2.0 ms native / 44 ms first / 138 ms final displayed**; it is not the first + user input. Its copied timing report contained no query text or source paths. +- Verified full projector installation produced the exact 554,821,024 bytes and + SHA-256 `c4a8a52691ecef40618438928bdf9e68379b854e24166f292592353db0aab64f`. + Generation 2 contained four chunks, including image content. The retained + CPU query worker stayed PID 397 through failure, retry and promotion. Explicit + selection of `retry.rs` remained selected when the image entered ahead of it. + +This interaction uncovered a separate semantic-activation timing defect: +full-media promotion reused the prior search measurement and displayed +**SHOWN 143,928 ms** despite an 8.0 ms native search. The copied report retained +`trigger: user_search` and the old three-chunk context. This value includes +media download/idle time and must not be presented as fresh search latency. +Watcher refresh timing was already corrected; semantic readiness/activation +needs the equivalent independent refresh measurement and another native retest. + +Local ignored evidence: `.searchi/qa/linux-earlytext-*.png`, +`linux-earlytext-{retry-search,promotion-search}.json`, `linux-earlytext-states.jsonl` +and `linux-earlytext-cpu-audit.json`. Native `desktop-qa-earlytext` retains proxy +events, logs and fixture data. The owned proxy helper is +`.searchi/linux-validation/opaque_proxy.py`. The private GPU-masked software +display audit found no GPU providers or descriptors; the query worker used Zen4 +CPU, one thread and zero swap. Both owned UI/proxy handles ended successfully, +Xvfb exited zero, and process inspection found no surviving app, worker, WebKit, +DBus, clipboard, display or proxy helpers. Outer `/dev/dxg` remained real. +Physical desktop, installed resource discovery, representative relevance, +first-install cold storage, p95, GPU and macOS gates remain open. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 44aa974..9e20b45 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -83,13 +83,17 @@ model loading must not be silently included in a warm-query claim. ## Reproducible performance validation -Automatic filesystem refreshes start a new frontend measurement at the refresh -event. `trigger: "filesystem_refresh"` distinguishes them from user searches; +Automatic filesystem and semantic-readiness/publication refreshes start a new +frontend measurement at the refresh event. `trigger: "filesystem_refresh"` or +`"semantic_activation"` distinguishes them from user searches; `SHOWN` and `latest_displayed_ms` exclude time spent idle before that event. `original_input` retains a fixed-size snapshot of the original debounce and display timings. Refreshes do not replay previous attempts or append ancestor traces. A refresh before the first response leaves that original response timing null rather than inventing a first-input measurement. +The refreshed context captures current generation/readiness/chunk counts rather +than retaining the pre-publication context. Semantic activation also invalidates +older queued responses and reruns the progressive lanes with the current filters. Record build profile, CPU/device and thread budget, VM/host resources, corpus and active generation, filters, cold/warm state, concurrent indexing and setup costs.