fix(engine): realistic worker memory budget + sizing/feedback telemetry

This commit is contained in:
Vance Ingalls
2026-07-21 14:31:14 -07:00
parent ed32898439
commit 12e599a6ba
11 changed files with 387 additions and 23 deletions
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { buildTelemetryJoinKeys } from "./feedback.js";
describe("buildTelemetryJoinKeys", () => {
it("emits fid + tid and omits renders when the ring is empty", () => {
const keys = buildTelemetryJoinKeys({
feedbackId: "feedback-uuid",
anonymousId: "install-uuid",
});
expect(keys).toBe("fid=feedback-uuid tid=install-uuid");
});
it("appends recent render ids newest-last with a ! marking failed renders", () => {
const keys = buildTelemetryJoinKeys({
feedbackId: "f",
anonymousId: "t",
recentRenders: [
{ id: "render-a", at: "2026-07-21T00:00:00Z", ok: true },
{ id: "render-b", at: "2026-07-21T01:00:00Z", ok: false },
],
});
expect(keys).toBe("fid=f tid=t renders=render-a,render-b!");
});
it("stays within the backend env cap for a full ring of uuid render ids", () => {
const uuid = "01234567-89ab-cdef-0123-456789abcdef";
const keys = buildTelemetryJoinKeys({
feedbackId: uuid,
anonymousId: uuid,
recentRenders: Array.from({ length: 5 }, (_, i) => ({
id: uuid,
at: "2026-07-21T00:00:00Z",
ok: i % 2 === 0,
})),
});
// submitFeedback caps env at 500 chars; the doctor summary consumes ~100.
expect(keys.length).toBeLessThan(400);
});
});
+43 -2
View File
@@ -1,4 +1,5 @@
import { failCommand } from "../utils/commandResult.js";
import { randomUUID } from "node:crypto";
import { resolve } from "node:path";
import { defineCommand } from "citty";
import * as clack from "@clack/prompts";
@@ -7,6 +8,7 @@ import type { Example } from "./_examples.js";
import { trackRenderFeedback } from "../telemetry/events.js";
import { shouldTrack, flush } from "../telemetry/client.js";
import { getDoctorSummary } from "../telemetry/feedback.js";
import { readConfig, type RecentRenderRecord } from "../telemetry/config.js";
import { publishProjectArchive } from "../utils/publishProject.js";
import { submitFeedback } from "../utils/submitFeedback.js";
import { buildIssueUrl, HYPERFRAMES_REPO_URL } from "../utils/feedbackIssue.js";
@@ -28,6 +30,27 @@ function normalizeComment(raw?: string): string | undefined {
return raw || undefined;
}
/**
* Compact PostHog join keys appended to the environment string that rides
* along with the forwarded report (and therefore lands verbatim in the wild
* feedback channel): `fid` = this submission's PostHog `survey sent`
* `feedback_id`; `tid` = the install's telemetry distinct_id; `renders` =
* recent `render_job_id`s (newest last, `!` suffix = the render failed).
* Together they turn a wild report into an exact telemetry lookup instead of
* a hardware-fingerprint hunt.
*/
export function buildTelemetryJoinKeys(input: {
feedbackId: string;
anonymousId: string;
recentRenders?: RecentRenderRecord[];
}): string {
const parts = [`fid=${input.feedbackId}`, `tid=${input.anonymousId}`];
if (input.recentRenders?.length) {
parts.push(`renders=${input.recentRenders.map((r) => `${r.id}${r.ok ? "" : "!"}`).join(",")}`);
}
return parts.join(" ");
}
function printIssueConsent(dir: string): void {
console.log();
console.log(
@@ -170,6 +193,18 @@ export default defineCommand({
const comment = normalizeComment(args.comment);
const doctorSummary = await getDoctorSummary();
// Join keys tying this report to the install's PostHog rows — see
// buildTelemetryJoinKeys. Appended to the env string so they surface in
// the forwarded report; mirrored as structured props on the PostHog event.
const feedbackId = randomUUID();
const config = readConfig();
const joinKeys = buildTelemetryJoinKeys({
feedbackId,
anonymousId: config.anonymousId,
recentRenders: config.recentRenders,
});
const envWithJoinKeys = doctorSummary ? `${doctorSummary} ${joinKeys}` : joinKeys;
// Soft-warn (never blocks) when the comment for a non-clean report is
// missing the mandated reproduction-packet markers. Prints before the
// submission ack so the reporter sees the nudge while their run is fresh.
@@ -177,13 +212,19 @@ export default defineCommand({
// The standalone command runs separately from `render`, so it has no real
// elapsed time to report. Omit it rather than recording a fake duration.
trackRenderFeedback({ rating, comment, doctorSummary });
trackRenderFeedback({
rating,
comment,
doctorSummary,
feedbackId,
recentRenderIds: config.recentRenders?.map((r) => r.id),
});
await flush();
// Ack first so the user isn't kept waiting on the best-effort forward (which
// is bounded to a few seconds and never surfaces an error either way).
console.log(c.dim("Thanks for the feedback!"));
await submitFeedback({ rating, comment, cliVersion: VERSION, env: doctorSummary });
await submitFeedback({ rating, comment, cliVersion: VERSION, env: envWithJoinKeys });
if (args["file-issue"] === true) {
await fileGithubIssue({
+19 -1
View File
@@ -61,7 +61,12 @@ import {
trackRenderObservation,
} from "../telemetry/events.js";
import { maybePromptRenderFeedback } from "../telemetry/feedback.js";
import { readConfigFresh, writeConfig, type HyperframesConfig } from "../telemetry/config.js";
import {
readConfigFresh,
recordRecentRender,
writeConfig,
type HyperframesConfig,
} from "../telemetry/config.js";
import { shouldTrack } from "../telemetry/client.js";
import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js";
import { bytesToMb } from "../telemetry/system.js";
@@ -1339,6 +1344,9 @@ function handleRenderError(
...renderJobObservabilityTelemetryPayload(job),
...getMemorySnapshot(),
});
// Failed renders join the recent-renders ring too — a bug report filed via
// `hyperframes feedback` is MOST likely to be about a failed render.
if (job?.id) recordRecentRender(job.id, false);
if (options.throwOnError) {
throw new Error(message);
}
@@ -1376,6 +1384,9 @@ function trackRenderMetrics(
options: RenderOptions,
docker: boolean,
): void {
// Successful render → recent-renders ring, so a later `hyperframes
// feedback` can attach this render's telemetry id to the report.
recordRecentRender(job.id, true);
const perf = job.perfSummary;
const compositionDurationMs = perf
? Math.round(perf.compositionDurationSeconds * 1000)
@@ -1393,6 +1404,13 @@ function trackRenderMetrics(
fps: fpsToNumber(options.fps),
quality: options.quality,
workers: options.workers ?? perf?.workers,
workersBoundBy: perf?.workerSizing?.boundBy,
workersCpuBased: perf?.workerSizing?.cpuBasedWorkers,
workersMemoryBased: perf?.workerSizing?.memoryBasedWorkers,
workersHeapBased: perf?.workerSizing?.heapBasedWorkers,
workersFrameBased: perf?.workerSizing?.frameBasedWorkers,
workersHeapLimitMb: perf?.workerSizing?.heapLimitMb,
workersExceedHeapAdvisory: perf?.workerSizing?.exceedsHeapAdvisory,
docker,
gpu: options.gpu,
authoringSkill: options.authoringSkill,