memorydetective 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/README.md +11 -5
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/leaksDebugStacks.d.ts +67 -0
- package/dist/parsers/leaksDebugStacks.js +187 -0
- package/dist/parsers/leaksDebugStacks.js.map +1 -0
- package/dist/parsers/referenceTree.js +5 -0
- package/dist/parsers/referenceTree.js.map +1 -1
- package/dist/runtime/prompts.js +49 -0
- package/dist/runtime/prompts.js.map +1 -1
- package/dist/tools/analyzeHangs.d.ts +28 -0
- package/dist/tools/analyzeHangs.js +136 -9
- package/dist/tools/analyzeHangs.js.map +1 -1
- package/dist/tools/captureScenarioState.d.ts +2 -2
- package/dist/tools/compareTracesByPattern.js +2 -0
- package/dist/tools/compareTracesByPattern.js.map +1 -1
- package/dist/tools/countAlive.d.ts +20 -4
- package/dist/tools/countAlive.js +91 -12
- package/dist/tools/countAlive.js.map +1 -1
- package/dist/tools/findRetainers.d.ts +15 -1
- package/dist/tools/findRetainers.js +51 -5
- package/dist/tools/findRetainers.js.map +1 -1
- package/dist/tools/summarizeTrace.d.ts +147 -0
- package/dist/tools/summarizeTrace.js +424 -0
- package/dist/tools/summarizeTrace.js.map +1 -0
- package/dist/tools/verifyFix.d.ts +23 -0
- package/dist/tools/verifyFix.js +121 -1
- package/dist/tools/verifyFix.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { runCommand } from "../runtime/exec.js";
|
|
2
3
|
import { runLeaksAndParse } from "../runtime/leaks.js";
|
|
4
|
+
import { parseLeaksDebugStacks, } from "../parsers/leaksDebugStacks.js";
|
|
3
5
|
import { suggestionGetDefinition } from "../runtime/suggestions.js";
|
|
4
6
|
export const findRetainersSchema = z.object({
|
|
5
7
|
path: z.string().min(1).describe("Absolute path to a `.memgraph` file."),
|
|
@@ -13,6 +15,10 @@ export const findRetainersSchema = z.object({
|
|
|
13
15
|
.positive()
|
|
14
16
|
.default(10)
|
|
15
17
|
.describe("Cap on how many retain chains to return (default 10)."),
|
|
18
|
+
includeReferenceTree: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe("v1.12+. When true, also run `leaks --debug=stacks --debug='<className>$'` to surface per-instance allocation stacks aggregated by call-stack fingerprint. Required on memgraphs where `leakCount: 0` and the class is reachable from KVO/NotificationCenter/caches (abandoned-memory shape). Each chain returns the allocation call stack + the unique retainer classes + a representative instance address. **Note:** `leaks --debug=stacks` only emits blocks for instances whose allocation stack was recorded, which requires the target was launched with `MallocStackLogging=1`. Xcode's Memory Graph Debugger export does NOT enable MSL by default, so memgraphs captured that way may surface fewer chains than the total instance count from `analyzeMemgraph.abandonedMemorySuspects[]`. Default false preserves v1.11 behavior."),
|
|
16
22
|
});
|
|
17
23
|
/**
|
|
18
24
|
* Walk the cycle forest and collect every parent-path that ends in a node whose
|
|
@@ -46,19 +52,59 @@ export function findRetainersIn(report, needle, maxResults = 10) {
|
|
|
46
52
|
visit(root, []);
|
|
47
53
|
return { totalMatches: total, retainers: matches };
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Spawn `leaks --debug=stacks --debug='<ClassName>$'` and parse the stdout
|
|
57
|
+
* into per-stack-fingerprint aggregated chains. Failure is non-fatal:
|
|
58
|
+
* returns an empty array so the cycle-side path still completes.
|
|
59
|
+
*
|
|
60
|
+
* leaks(1) `--debug=` predicate rejects `^` (`cannot match the start of
|
|
61
|
+
* a class name`); only the `$` trailing anchor is supported. The
|
|
62
|
+
* resulting semantic is "ends with X", which matches AVPlayerItem
|
|
63
|
+
* exactly but also things like MyAVPlayerItem (rare; intentionally
|
|
64
|
+
* permissive over over-restrictive). Class-name regex metacharacters
|
|
65
|
+
* are escaped so substrings like "Player.Item" stay literal.
|
|
66
|
+
*/
|
|
67
|
+
async function captureReferenceTreeChains(path, className, maxResults) {
|
|
68
|
+
// Escape regex metacharacters in the user-supplied class name so a
|
|
69
|
+
// substring like "AVPlayerItem" stays literal under leaks's regex
|
|
70
|
+
// predicate. `^` and `$` aren't escaped (leaks treats them specially).
|
|
71
|
+
const escaped = className.replace(/[.*+?{}()|[\]\\]/g, "\\$&");
|
|
72
|
+
const predicate = `${escaped}$`;
|
|
73
|
+
const result = await runCommand("leaks", ["--debug=stacks", `--debug=${predicate}`, path], { timeoutMs: 5 * 60_000 });
|
|
74
|
+
if (result.code !== 0 && result.code !== 1)
|
|
75
|
+
return [];
|
|
76
|
+
const all = parseLeaksDebugStacks(result.stdout);
|
|
77
|
+
return all.slice(0, maxResults);
|
|
78
|
+
}
|
|
49
79
|
export async function findRetainers(input) {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
80
|
+
const wantReferenceTree = input.includeReferenceTree ?? false;
|
|
81
|
+
const maxResults = input.maxResults ?? 10;
|
|
82
|
+
const [{ report, resolvedPath }, referenceTreeChains,] = await Promise.all([
|
|
83
|
+
runLeaksAndParse(input.path),
|
|
84
|
+
wantReferenceTree
|
|
85
|
+
? captureReferenceTreeChains(input.path, input.className, maxResults)
|
|
86
|
+
: Promise.resolve([]),
|
|
87
|
+
]);
|
|
88
|
+
const { totalMatches, retainers } = findRetainersIn(report, input.className, maxResults);
|
|
89
|
+
// Update totalMatches to include reference-tree side if it found instances
|
|
90
|
+
// the cycle path missed. Instance counts aggregate across the per-stack
|
|
91
|
+
// chains; cycle matches count each path separately, so we don't double-add.
|
|
92
|
+
const referenceTreeInstanceTotal = referenceTreeChains.reduce((s, c) => s + c.instanceCount, 0);
|
|
93
|
+
const effectiveTotal = totalMatches > 0 ? totalMatches : referenceTreeInstanceTotal;
|
|
94
|
+
const suggestedNextCalls = effectiveTotal > 0
|
|
53
95
|
? [suggestionGetDefinition({ symbolName: input.className })]
|
|
54
96
|
: [];
|
|
55
|
-
|
|
97
|
+
const result = {
|
|
56
98
|
ok: true,
|
|
57
99
|
path: resolvedPath,
|
|
58
100
|
className: input.className,
|
|
59
|
-
totalMatches,
|
|
101
|
+
totalMatches: effectiveTotal,
|
|
60
102
|
retainers,
|
|
61
103
|
...(suggestedNextCalls.length > 0 ? { suggestedNextCalls } : {}),
|
|
62
104
|
};
|
|
105
|
+
if (referenceTreeChains.length > 0) {
|
|
106
|
+
result.referenceTreeChains = referenceTreeChains;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
63
109
|
}
|
|
64
110
|
//# sourceMappingURL=findRetainers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findRetainers.js","sourceRoot":"","sources":["../../src/tools/findRetainers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAGpE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sCAAsC,CAAC;IACxE,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CACP,4EAA4E,CAC7E;IACH,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,uDAAuD,CAAC;
|
|
1
|
+
{"version":3,"file":"findRetainers.js","sourceRoot":"","sources":["../../src/tools/findRetainers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `summarizeTrace`: the trace-to-summary-card-in-one-call feature.
|
|
3
|
+
*
|
|
4
|
+
* Today an agent handed a `.trace` chains inspectTrace + up to 5
|
|
5
|
+
* analyzers + reasons over tens of KB of JSON. That's 6 round-trips
|
|
6
|
+
* and ~$0.10-0.20 in tokens, most of which get thrown away after the
|
|
7
|
+
* agent identifies the one user-visible finding.
|
|
8
|
+
*
|
|
9
|
+
* summarizeTrace does it all in one call:
|
|
10
|
+
*
|
|
11
|
+
* 1. Inspects the TOC via inspectTrace (reuses the v1.11 path).
|
|
12
|
+
* 2. For each populated known schema, runs the matching analyzer in
|
|
13
|
+
* parallel with smart defaults tuned for "what would a user care
|
|
14
|
+
* about?" (Apple's 100ms hitch threshold, top-10 hangs, top-15
|
|
15
|
+
* time-profile symbols, etc).
|
|
16
|
+
* 3. Cross-correlates findings (v1.13 Phase 2): hangs overlapping
|
|
17
|
+
* with hitches, allocation spikes preceding hangs, etc.
|
|
18
|
+
* 4. Produces a structured result PLUS a pre-rendered compact
|
|
19
|
+
* markdown card (< 10 KB target) suitable for direct presentation
|
|
20
|
+
* to the user without further reasoning.
|
|
21
|
+
*
|
|
22
|
+
* Strategic positioning: this is memorydetective's "synthesis over
|
|
23
|
+
* raw-query" play vs trace-MCPs that go deep on single-schema access.
|
|
24
|
+
* See `~/Desktop/internal/v1.9-notelet-retro-market.md` §4.5 for the
|
|
25
|
+
* full framing.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { type InspectTraceResult } from "./inspectTrace.js";
|
|
29
|
+
import { type AnalyzeHangsResult } from "./analyzeHangs.js";
|
|
30
|
+
import { type AnalyzeAnimationHitchesResult } from "./analyzeAnimationHitches.js";
|
|
31
|
+
import { type AnalyzeTimeProfileResult } from "./analyzeTimeProfile.js";
|
|
32
|
+
import { type AnalyzeAllocationsResult } from "./analyzeAllocations.js";
|
|
33
|
+
import { type AnalyzeAppLaunchResult } from "./analyzeAppLaunch.js";
|
|
34
|
+
export declare const summarizeTraceSchema: z.ZodObject<{
|
|
35
|
+
tracePath: z.ZodString;
|
|
36
|
+
focus: z.ZodDefault<z.ZodEnum<["hangs", "hitches", "allocations", "launch", "all"]>>;
|
|
37
|
+
verbose: z.ZodDefault<z.ZodBoolean>;
|
|
38
|
+
}, "strip", z.ZodTypeAny, {
|
|
39
|
+
tracePath: string;
|
|
40
|
+
focus: "all" | "allocations" | "hangs" | "hitches" | "launch";
|
|
41
|
+
verbose: boolean;
|
|
42
|
+
}, {
|
|
43
|
+
tracePath: string;
|
|
44
|
+
focus?: "all" | "allocations" | "hangs" | "hitches" | "launch" | undefined;
|
|
45
|
+
verbose?: boolean | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
export type SummarizeTraceInput = z.infer<typeof summarizeTraceSchema>;
|
|
48
|
+
/**
|
|
49
|
+
* Per-analyzer entry on the structured result. `status` distinguishes
|
|
50
|
+
* "ran successfully", "schema absent in trace", and "ran but failed".
|
|
51
|
+
* Callers branching on the summary can decide whether to retry / refine.
|
|
52
|
+
*/
|
|
53
|
+
export interface SummarizeAreaSummary<TResult> {
|
|
54
|
+
status: "ok" | "schema-absent" | "failed";
|
|
55
|
+
/** Why the status is what it is (one sentence). Surfaces SIGSEGV / missing-schema / parser-error reasons. */
|
|
56
|
+
diagnosis: string;
|
|
57
|
+
/** Full analyzer result when status === "ok". Useful when a caller wants to drill in without re-running the analyzer. */
|
|
58
|
+
result?: TResult;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* v1.13 Phase 2: cross-area correlation. Each entry is a finding
|
|
62
|
+
* tying TWO areas together via timestamp overlap. The narrative
|
|
63
|
+
* field is the human-scannable string that goes into the markdown
|
|
64
|
+
* card; the structured fields (`kind`, `confidence`, evidence ids)
|
|
65
|
+
* are what callers can branch on programmatically.
|
|
66
|
+
*/
|
|
67
|
+
export interface Correlation {
|
|
68
|
+
/** Which two areas this correlation ties together. Currently only `hangs+hitches` is supported; `hangs+allocations` and `hitches+allocations` are deferred to v1.14+ because the analyzer doesn't currently expose per-timestamp allocation rows. */
|
|
69
|
+
kind: "hangs+hitches";
|
|
70
|
+
/** `high` when the overlap is substantial (both events > 100ms and the windows overlap significantly); `medium` when one event is short; `low` when timestamps are only adjacent. */
|
|
71
|
+
confidence: "high" | "medium" | "low";
|
|
72
|
+
/** Pre-formatted human-scannable narrative. Goes into the markdown card. */
|
|
73
|
+
narrative: string;
|
|
74
|
+
/** Start time in seconds (for the hang event). Used to rank correlations by user-relevance (earliest first). */
|
|
75
|
+
atSec: number;
|
|
76
|
+
}
|
|
77
|
+
export interface SummarizeTraceResult {
|
|
78
|
+
ok: boolean;
|
|
79
|
+
tracePath: string;
|
|
80
|
+
/** TOC + suggestedNextCalls from inspectTrace. Always present. */
|
|
81
|
+
inspection: InspectTraceResult;
|
|
82
|
+
/** Per-area summaries. Each section is independent; absence of one doesn't fail the call. */
|
|
83
|
+
areas: {
|
|
84
|
+
hangs: SummarizeAreaSummary<AnalyzeHangsResult>;
|
|
85
|
+
hitches: SummarizeAreaSummary<AnalyzeAnimationHitchesResult>;
|
|
86
|
+
timeProfile: SummarizeAreaSummary<AnalyzeTimeProfileResult>;
|
|
87
|
+
allocations: SummarizeAreaSummary<AnalyzeAllocationsResult>;
|
|
88
|
+
appLaunch: SummarizeAreaSummary<AnalyzeAppLaunchResult>;
|
|
89
|
+
};
|
|
90
|
+
/** Cross-area correlations (v1.13 Phase 2). Empty when areas don't have enough data to correlate. */
|
|
91
|
+
correlations: Correlation[];
|
|
92
|
+
/** Headline string: 1-2 sentences naming the biggest user-impact finding across all areas. */
|
|
93
|
+
headline: string;
|
|
94
|
+
/** Pre-rendered markdown summary card. Target < 10 KB at default `verbose: false`. */
|
|
95
|
+
markdown: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Pure: detect hangs whose window overlaps with animation hitches.
|
|
99
|
+
* When a user sees a hang AND a hitch in the same time window,
|
|
100
|
+
* they almost certainly perceived the impact (the main-thread block
|
|
101
|
+
* delayed render commits, dropping frames).
|
|
102
|
+
*
|
|
103
|
+
* The overlap check is symmetric: a hitch can fall within a hang's
|
|
104
|
+
* window OR a hang can fall within a hitch's window. Both directions
|
|
105
|
+
* are treated equally.
|
|
106
|
+
*
|
|
107
|
+
* Confidence:
|
|
108
|
+
*
|
|
109
|
+
* - `high`: both events >= 250ms AND the overlap span >= 100ms.
|
|
110
|
+
* - `medium`: at least one event >= 250ms.
|
|
111
|
+
* - `low`: neither event >= 250ms but the windows touch.
|
|
112
|
+
*
|
|
113
|
+
* Results are sorted by `atSec` ascending so the markdown card lists
|
|
114
|
+
* correlations in trace order.
|
|
115
|
+
*/
|
|
116
|
+
export declare function correlateHangsAndHitches(hangs: Array<{
|
|
117
|
+
startNs: number;
|
|
118
|
+
durationNs: number;
|
|
119
|
+
durationMs: number;
|
|
120
|
+
}>, hitches: Array<{
|
|
121
|
+
startNs: number;
|
|
122
|
+
durationNs: number;
|
|
123
|
+
durationMs: number;
|
|
124
|
+
hitchType?: string;
|
|
125
|
+
}>): Correlation[];
|
|
126
|
+
/**
|
|
127
|
+
* Pure: build all cross-area correlations from per-area summaries.
|
|
128
|
+
* Currently only `hangs+hitches` produces entries; allocation-based
|
|
129
|
+
* correlations need per-timestamp allocation data the existing
|
|
130
|
+
* analyzeAllocations doesn't expose (v1.14+ candidate).
|
|
131
|
+
*/
|
|
132
|
+
export declare function buildCorrelations(areas: SummarizeTraceResult["areas"]): Correlation[];
|
|
133
|
+
/**
|
|
134
|
+
* Pure: produce the one-or-two-sentence headline that goes at the top
|
|
135
|
+
* of the markdown card. Picks the most user-visible finding across
|
|
136
|
+
* all areas. Order of priority: longest hang above 250ms > worst
|
|
137
|
+
* launch phase > worst hitch > largest allocation spike.
|
|
138
|
+
*/
|
|
139
|
+
export declare function buildHeadline(areas: SummarizeTraceResult["areas"]): string;
|
|
140
|
+
/**
|
|
141
|
+
* Pure: assemble the compact markdown summary card. Designed to be
|
|
142
|
+
* <10 KB at default settings. The structured `areas` field carries
|
|
143
|
+
* the full data for callers who need it; this is the human view.
|
|
144
|
+
*/
|
|
145
|
+
export declare function buildMarkdownCard(result: Omit<SummarizeTraceResult, "markdown">, verbose: boolean): string;
|
|
146
|
+
export declare function summarizeTrace(input: SummarizeTraceInput): Promise<SummarizeTraceResult>;
|
|
147
|
+
export declare const SUMMARIZE_AREA_KEYS: readonly ["hangs", "hitches", "timeProfile", "allocations", "appLaunch"];
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `summarizeTrace`: the trace-to-summary-card-in-one-call feature.
|
|
3
|
+
*
|
|
4
|
+
* Today an agent handed a `.trace` chains inspectTrace + up to 5
|
|
5
|
+
* analyzers + reasons over tens of KB of JSON. That's 6 round-trips
|
|
6
|
+
* and ~$0.10-0.20 in tokens, most of which get thrown away after the
|
|
7
|
+
* agent identifies the one user-visible finding.
|
|
8
|
+
*
|
|
9
|
+
* summarizeTrace does it all in one call:
|
|
10
|
+
*
|
|
11
|
+
* 1. Inspects the TOC via inspectTrace (reuses the v1.11 path).
|
|
12
|
+
* 2. For each populated known schema, runs the matching analyzer in
|
|
13
|
+
* parallel with smart defaults tuned for "what would a user care
|
|
14
|
+
* about?" (Apple's 100ms hitch threshold, top-10 hangs, top-15
|
|
15
|
+
* time-profile symbols, etc).
|
|
16
|
+
* 3. Cross-correlates findings (v1.13 Phase 2): hangs overlapping
|
|
17
|
+
* with hitches, allocation spikes preceding hangs, etc.
|
|
18
|
+
* 4. Produces a structured result PLUS a pre-rendered compact
|
|
19
|
+
* markdown card (< 10 KB target) suitable for direct presentation
|
|
20
|
+
* to the user without further reasoning.
|
|
21
|
+
*
|
|
22
|
+
* Strategic positioning: this is memorydetective's "synthesis over
|
|
23
|
+
* raw-query" play vs trace-MCPs that go deep on single-schema access.
|
|
24
|
+
* See `~/Desktop/internal/v1.9-notelet-retro-market.md` §4.5 for the
|
|
25
|
+
* full framing.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { resolve as resolvePath, basename } from "node:path";
|
|
30
|
+
import { inspectTrace } from "./inspectTrace.js";
|
|
31
|
+
import { analyzeHangs, } from "./analyzeHangs.js";
|
|
32
|
+
import { analyzeAnimationHitches, } from "./analyzeAnimationHitches.js";
|
|
33
|
+
import { analyzeTimeProfile, } from "./analyzeTimeProfile.js";
|
|
34
|
+
import { analyzeAllocations, } from "./analyzeAllocations.js";
|
|
35
|
+
import { analyzeAppLaunch, } from "./analyzeAppLaunch.js";
|
|
36
|
+
export const summarizeTraceSchema = z.object({
|
|
37
|
+
tracePath: z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.describe("Absolute path to a `.trace` bundle (output of `xcrun xctrace record` or Instruments)."),
|
|
41
|
+
focus: z
|
|
42
|
+
.enum(["hangs", "hitches", "allocations", "launch", "all"])
|
|
43
|
+
.default("all")
|
|
44
|
+
.describe("When set to a specific area, the summary card emphasizes that area and downplays others. Useful for piping into more focused agent loops. Default `all`."),
|
|
45
|
+
verbose: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.default(false)
|
|
48
|
+
.describe("When true, the markdown card includes the full top-N per area (15+ rows per section) instead of the default 5. Trade-off: card grows from <10 KB to potentially 30+ KB."),
|
|
49
|
+
});
|
|
50
|
+
const DEFAULT_HITCH_THRESHOLD_MS = 100; // Apple's user-perceptible threshold.
|
|
51
|
+
const DEFAULT_HANG_MIN_MS = 100;
|
|
52
|
+
const DEFAULT_TOP_N_HANGS = 10;
|
|
53
|
+
const DEFAULT_TOP_N_HITCHES = 10;
|
|
54
|
+
const DEFAULT_TOP_N_TIME_PROFILE = 15;
|
|
55
|
+
const DEFAULT_TOP_N_ALLOCATIONS = 10;
|
|
56
|
+
/**
|
|
57
|
+
* Build a per-area summary by running an analyzer with smart defaults
|
|
58
|
+
* and wrapping the outcome (success / schema-absent / failed) into a
|
|
59
|
+
* status-tagged struct. The schema-absent branch reads the inspectTrace
|
|
60
|
+
* `rowCounts` so we don't spawn xctrace for empty schemas.
|
|
61
|
+
*/
|
|
62
|
+
async function buildAreaSummary(schemaName, inspection, runner, schemaAbsentDiagnosis) {
|
|
63
|
+
const rowCount = inspection.rowCounts[schemaName] ?? 0;
|
|
64
|
+
if (rowCount === 0) {
|
|
65
|
+
return {
|
|
66
|
+
status: "schema-absent",
|
|
67
|
+
diagnosis: schemaAbsentDiagnosis,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const result = await runner();
|
|
72
|
+
return {
|
|
73
|
+
status: "ok",
|
|
74
|
+
diagnosis: `${rowCount.toLocaleString()} rows analyzed.`,
|
|
75
|
+
result,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
status: "failed",
|
|
81
|
+
diagnosis: `Analyzer failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Pure: detect hangs whose window overlaps with animation hitches.
|
|
87
|
+
* When a user sees a hang AND a hitch in the same time window,
|
|
88
|
+
* they almost certainly perceived the impact (the main-thread block
|
|
89
|
+
* delayed render commits, dropping frames).
|
|
90
|
+
*
|
|
91
|
+
* The overlap check is symmetric: a hitch can fall within a hang's
|
|
92
|
+
* window OR a hang can fall within a hitch's window. Both directions
|
|
93
|
+
* are treated equally.
|
|
94
|
+
*
|
|
95
|
+
* Confidence:
|
|
96
|
+
*
|
|
97
|
+
* - `high`: both events >= 250ms AND the overlap span >= 100ms.
|
|
98
|
+
* - `medium`: at least one event >= 250ms.
|
|
99
|
+
* - `low`: neither event >= 250ms but the windows touch.
|
|
100
|
+
*
|
|
101
|
+
* Results are sorted by `atSec` ascending so the markdown card lists
|
|
102
|
+
* correlations in trace order.
|
|
103
|
+
*/
|
|
104
|
+
export function correlateHangsAndHitches(hangs, hitches) {
|
|
105
|
+
const results = [];
|
|
106
|
+
for (const hang of hangs) {
|
|
107
|
+
const hangEnd = hang.startNs + hang.durationNs;
|
|
108
|
+
for (const hitch of hitches) {
|
|
109
|
+
const hitchEnd = hitch.startNs + hitch.durationNs;
|
|
110
|
+
// Half-open interval overlap: max(starts) < min(ends).
|
|
111
|
+
const overlapStart = Math.max(hang.startNs, hitch.startNs);
|
|
112
|
+
const overlapEnd = Math.min(hangEnd, hitchEnd);
|
|
113
|
+
if (overlapEnd <= overlapStart)
|
|
114
|
+
continue;
|
|
115
|
+
const overlapMs = (overlapEnd - overlapStart) / 1e6;
|
|
116
|
+
const atSec = Math.min(hang.startNs, hitch.startNs) / 1e9;
|
|
117
|
+
let confidence;
|
|
118
|
+
if (hang.durationMs >= 250 &&
|
|
119
|
+
hitch.durationMs >= 250 &&
|
|
120
|
+
overlapMs >= 100) {
|
|
121
|
+
confidence = "high";
|
|
122
|
+
}
|
|
123
|
+
else if (hang.durationMs >= 250 || hitch.durationMs >= 250) {
|
|
124
|
+
confidence = "medium";
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
confidence = "low";
|
|
128
|
+
}
|
|
129
|
+
const hitchKind = hitch.hitchType ? `${hitch.hitchType} ` : "";
|
|
130
|
+
const narrative = `Hang at t=${(hang.startNs / 1e9).toFixed(2)}s (${hang.durationMs.toFixed(0)}ms) overlaps with ${hitchKind}hitch at t=${(hitch.startNs / 1e9).toFixed(2)}s (${hitch.durationMs.toFixed(0)}ms). Main-thread block likely caused the dropped frames.`;
|
|
131
|
+
results.push({ kind: "hangs+hitches", confidence, narrative, atSec });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
results.sort((a, b) => a.atSec - b.atSec);
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Pure: build all cross-area correlations from per-area summaries.
|
|
139
|
+
* Currently only `hangs+hitches` produces entries; allocation-based
|
|
140
|
+
* correlations need per-timestamp allocation data the existing
|
|
141
|
+
* analyzeAllocations doesn't expose (v1.14+ candidate).
|
|
142
|
+
*/
|
|
143
|
+
export function buildCorrelations(areas) {
|
|
144
|
+
const hangs = areas.hangs.result?.top ?? [];
|
|
145
|
+
const hitches = areas.hitches.result?.top ?? [];
|
|
146
|
+
if (hangs.length === 0 || hitches.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
return correlateHangsAndHitches(hangs, hitches);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Pure: produce the one-or-two-sentence headline that goes at the top
|
|
152
|
+
* of the markdown card. Picks the most user-visible finding across
|
|
153
|
+
* all areas. Order of priority: longest hang above 250ms > worst
|
|
154
|
+
* launch phase > worst hitch > largest allocation spike.
|
|
155
|
+
*/
|
|
156
|
+
export function buildHeadline(areas) {
|
|
157
|
+
const hang = areas.hangs.result?.top?.[0];
|
|
158
|
+
if (hang && hang.durationMs >= 250) {
|
|
159
|
+
const violation = hang.mainThreadViolations?.[0];
|
|
160
|
+
const causedBy = violation
|
|
161
|
+
? ` (caused by \`${violation.topFrame}\` -> ${violation.kind})`
|
|
162
|
+
: "";
|
|
163
|
+
return `${hang.durationMs.toFixed(0)}ms hang at t=${(hang.startNs / 1e9).toFixed(2)}s${causedBy}. Likely user-visible freeze.`;
|
|
164
|
+
}
|
|
165
|
+
const launch = areas.appLaunch.result;
|
|
166
|
+
if (launch && launch.totalLaunchMs > 1000) {
|
|
167
|
+
return `Launch took ${launch.totalLaunchMs.toFixed(0)}ms (${launch.launchType} launch). Above the 1s user-visible threshold.`;
|
|
168
|
+
}
|
|
169
|
+
const hitchesPerceptible = areas.hitches.result?.totals?.perceptible ?? 0;
|
|
170
|
+
if (hitchesPerceptible > 0) {
|
|
171
|
+
return `${hitchesPerceptible} animation hitch${hitchesPerceptible === 1 ? "" : "es"} above the 100ms user-perceptible threshold. Investigate render-server commits and main-thread work during scroll.`;
|
|
172
|
+
}
|
|
173
|
+
if (hang) {
|
|
174
|
+
return `${hang.durationMs.toFixed(0)}ms hang at t=${(hang.startNs / 1e9).toFixed(2)}s. Below the 250ms user-visible threshold but still worth investigating.`;
|
|
175
|
+
}
|
|
176
|
+
return "No user-perceptible perf events detected in the analyzed schemas.";
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Pure: assemble the compact markdown summary card. Designed to be
|
|
180
|
+
* <10 KB at default settings. The structured `areas` field carries
|
|
181
|
+
* the full data for callers who need it; this is the human view.
|
|
182
|
+
*/
|
|
183
|
+
export function buildMarkdownCard(result, verbose) {
|
|
184
|
+
const sections = [];
|
|
185
|
+
const inspection = result.inspection;
|
|
186
|
+
const traceName = basename(result.tracePath);
|
|
187
|
+
sections.push(`# Trace summary: ${traceName}`);
|
|
188
|
+
sections.push("");
|
|
189
|
+
const meta = [];
|
|
190
|
+
if (inspection.deviceModel)
|
|
191
|
+
meta.push(inspection.deviceModel);
|
|
192
|
+
if (inspection.osVersion)
|
|
193
|
+
meta.push(inspection.osVersion);
|
|
194
|
+
if (inspection.templateName)
|
|
195
|
+
meta.push(`Template: \`${inspection.templateName}\``);
|
|
196
|
+
if (meta.length > 0)
|
|
197
|
+
sections.push(`**${meta.join(" · ")}**`);
|
|
198
|
+
sections.push("");
|
|
199
|
+
sections.push(`> **Headline:** ${result.headline}`);
|
|
200
|
+
sections.push("");
|
|
201
|
+
const topNHangs = verbose ? DEFAULT_TOP_N_HANGS : 5;
|
|
202
|
+
const topNHitches = verbose ? DEFAULT_TOP_N_HITCHES : 5;
|
|
203
|
+
const topNAllocations = verbose ? DEFAULT_TOP_N_ALLOCATIONS : 5;
|
|
204
|
+
const topNTimeProfile = verbose ? DEFAULT_TOP_N_TIME_PROFILE : 5;
|
|
205
|
+
// Hangs section.
|
|
206
|
+
if (result.areas.hangs.status === "ok" && result.areas.hangs.result) {
|
|
207
|
+
const h = result.areas.hangs.result;
|
|
208
|
+
const totalHangs = h.totals?.hangs ?? 0;
|
|
209
|
+
const totalMicrohangs = h.totals?.microhangs ?? 0;
|
|
210
|
+
const userVisible = (h.top ?? []).filter((e) => e.durationMs >= 250).length;
|
|
211
|
+
sections.push(`## Hangs (${totalHangs}, ${userVisible} user-visible, ${totalMicrohangs} microhang${totalMicrohangs === 1 ? "" : "s"})`);
|
|
212
|
+
sections.push("");
|
|
213
|
+
if ((h.top ?? []).length > 0) {
|
|
214
|
+
for (const entry of (h.top ?? []).slice(0, topNHangs)) {
|
|
215
|
+
const at = (entry.startNs / 1e9).toFixed(2);
|
|
216
|
+
const violation = entry.mainThreadViolations?.[0];
|
|
217
|
+
const classification = violation
|
|
218
|
+
? ` → ${violation.kind} (\`${violation.topFrame}\`)`
|
|
219
|
+
: "";
|
|
220
|
+
sections.push(`- ${entry.durationMs.toFixed(0)}ms at t=${at}s${classification}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
sections.push("_No hang events above the minimum threshold._");
|
|
225
|
+
}
|
|
226
|
+
sections.push("");
|
|
227
|
+
}
|
|
228
|
+
else if (result.areas.hangs.status === "schema-absent") {
|
|
229
|
+
// Suppressed when no hangs data; reduces card clutter.
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
sections.push(`## Hangs`);
|
|
233
|
+
sections.push("");
|
|
234
|
+
sections.push(`_${result.areas.hangs.diagnosis}_`);
|
|
235
|
+
sections.push("");
|
|
236
|
+
}
|
|
237
|
+
// Animation hitches section.
|
|
238
|
+
if (result.areas.hitches.status === "ok" && result.areas.hitches.result) {
|
|
239
|
+
const h = result.areas.hitches.result;
|
|
240
|
+
const totalHitches = h.totals?.rows ?? 0;
|
|
241
|
+
const perceptible = h.totals?.perceptible ?? 0;
|
|
242
|
+
sections.push(`## Animation hitches (${totalHitches}, ${perceptible} above 100ms)`);
|
|
243
|
+
sections.push("");
|
|
244
|
+
if ((h.top ?? []).length > 0) {
|
|
245
|
+
sections.push("| At | Duration | Type |");
|
|
246
|
+
sections.push("|---|---:|---|");
|
|
247
|
+
for (const entry of (h.top ?? []).slice(0, topNHitches)) {
|
|
248
|
+
const at = `t=${(entry.startNs / 1e9).toFixed(2)}s`;
|
|
249
|
+
sections.push(`| ${at} | ${entry.durationMs.toFixed(0)}ms | ${entry.hitchType || "—"} |`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
sections.push("");
|
|
253
|
+
}
|
|
254
|
+
else if (result.areas.hitches.status === "failed") {
|
|
255
|
+
sections.push(`## Animation hitches`);
|
|
256
|
+
sections.push("");
|
|
257
|
+
sections.push(`_${result.areas.hitches.diagnosis}_`);
|
|
258
|
+
sections.push("");
|
|
259
|
+
}
|
|
260
|
+
// Time profile section. analyzeTimeProfile may surface a workaround
|
|
261
|
+
// notice (xctrace SIGSEGV) via `notice`; we surface it inline so the
|
|
262
|
+
// summary card flags the partial-data situation.
|
|
263
|
+
if (result.areas.timeProfile.status === "ok" &&
|
|
264
|
+
result.areas.timeProfile.result) {
|
|
265
|
+
const tp = result.areas.timeProfile.result;
|
|
266
|
+
sections.push(`## Time profile (${tp.totalSamples.toLocaleString()} samples, top ${topNTimeProfile} symbols)`);
|
|
267
|
+
sections.push("");
|
|
268
|
+
if (tp.notice) {
|
|
269
|
+
sections.push(`> _${tp.notice}_`);
|
|
270
|
+
sections.push("");
|
|
271
|
+
}
|
|
272
|
+
const symbols = tp.topSymbols ?? [];
|
|
273
|
+
if (symbols.length > 0) {
|
|
274
|
+
for (const s of symbols.slice(0, topNTimeProfile)) {
|
|
275
|
+
const pct = tp.totalSamples > 0
|
|
276
|
+
? `${((s.samples / tp.totalSamples) * 100).toFixed(1)}%`
|
|
277
|
+
: `${s.samples} samples`;
|
|
278
|
+
sections.push(`- ${pct} \`${s.symbol || "???"}\``);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
sections.push("_No symbols above the noise threshold._");
|
|
283
|
+
}
|
|
284
|
+
sections.push("");
|
|
285
|
+
}
|
|
286
|
+
else if (result.areas.timeProfile.status === "failed") {
|
|
287
|
+
sections.push(`## Time profile`);
|
|
288
|
+
sections.push("");
|
|
289
|
+
sections.push(`_${result.areas.timeProfile.diagnosis}_`);
|
|
290
|
+
sections.push("");
|
|
291
|
+
}
|
|
292
|
+
// Allocations section.
|
|
293
|
+
if (result.areas.allocations.status === "ok" &&
|
|
294
|
+
result.areas.allocations.result) {
|
|
295
|
+
const a = result.areas.allocations.result;
|
|
296
|
+
const cumulativeBytes = a.totals.cumulativeBytes;
|
|
297
|
+
const persistentBytes = a.totals.persistentBytes;
|
|
298
|
+
sections.push(`## Allocations (${(cumulativeBytes / 1024 / 1024).toFixed(1)} MB cumulative, ${(persistentBytes / 1024 / 1024).toFixed(1)} MB persistent)`);
|
|
299
|
+
sections.push("");
|
|
300
|
+
const top = a.topByBytes ?? [];
|
|
301
|
+
if (top.length > 0) {
|
|
302
|
+
sections.push("| Category | Lifecycle | Bytes (cumulative) | Count |");
|
|
303
|
+
sections.push("|---|---|---:|---:|");
|
|
304
|
+
for (const entry of top.slice(0, topNAllocations)) {
|
|
305
|
+
const mb = (entry.cumulativeBytes / 1024 / 1024).toFixed(2);
|
|
306
|
+
sections.push(`| \`${entry.category}\` | ${entry.lifecycle} | ${mb} MB | ${entry.cumulativeCount.toLocaleString()} |`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
sections.push("");
|
|
310
|
+
}
|
|
311
|
+
// App launch section.
|
|
312
|
+
if (result.areas.appLaunch.status === "ok" &&
|
|
313
|
+
result.areas.appLaunch.result) {
|
|
314
|
+
const al = result.areas.appLaunch.result;
|
|
315
|
+
sections.push(`## App launch (${al.launchType}, ${al.totalLaunchMs.toFixed(0)}ms total)`);
|
|
316
|
+
sections.push("");
|
|
317
|
+
if ((al.phases ?? []).length > 0) {
|
|
318
|
+
sections.push("| Phase | Duration | % of total |");
|
|
319
|
+
sections.push("|---|---:|---:|");
|
|
320
|
+
for (const p of al.phases ?? []) {
|
|
321
|
+
sections.push(`| ${p.label || p.phase} | ${p.durationMs.toFixed(0)}ms | ${p.percentOfTotal.toFixed(1)}% |`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
sections.push("");
|
|
325
|
+
}
|
|
326
|
+
// Cross-correlations (v1.13 Phase 2). High and medium go straight
|
|
327
|
+
// into the card; low-confidence entries collapsed into a single
|
|
328
|
+
// "plus N more" line to keep the card compact.
|
|
329
|
+
const correlations = result.correlations ?? [];
|
|
330
|
+
if (correlations.length > 0) {
|
|
331
|
+
const highMedium = correlations.filter((c) => c.confidence === "high" || c.confidence === "medium");
|
|
332
|
+
const low = correlations.filter((c) => c.confidence === "low");
|
|
333
|
+
if (highMedium.length > 0 || verbose) {
|
|
334
|
+
sections.push("## Cross-correlations");
|
|
335
|
+
sections.push("");
|
|
336
|
+
const visible = verbose ? correlations : highMedium;
|
|
337
|
+
for (const c of visible) {
|
|
338
|
+
const confidenceBadge = c.confidence === "high"
|
|
339
|
+
? "**HIGH**"
|
|
340
|
+
: c.confidence === "medium"
|
|
341
|
+
? "MEDIUM"
|
|
342
|
+
: "low";
|
|
343
|
+
sections.push(`- (${confidenceBadge}) ${c.narrative}`);
|
|
344
|
+
}
|
|
345
|
+
if (!verbose && low.length > 0) {
|
|
346
|
+
sections.push(`- _${low.length} low-confidence overlap${low.length === 1 ? "" : "s"} omitted; pass \`verbose: true\` to see them._`);
|
|
347
|
+
}
|
|
348
|
+
sections.push("");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Suggested next calls from inspectTrace (carries them already).
|
|
352
|
+
if (inspection.suggestedNextCalls.length > 0) {
|
|
353
|
+
sections.push("## Suggested next calls");
|
|
354
|
+
sections.push("");
|
|
355
|
+
for (const call of inspection.suggestedNextCalls.slice(0, 5)) {
|
|
356
|
+
sections.push(`- \`${call.tool}\` — ${call.why}`);
|
|
357
|
+
}
|
|
358
|
+
sections.push("");
|
|
359
|
+
}
|
|
360
|
+
return sections.join("\n").trim();
|
|
361
|
+
}
|
|
362
|
+
export async function summarizeTrace(input) {
|
|
363
|
+
const tracePath = resolvePath(input.tracePath);
|
|
364
|
+
if (!existsSync(tracePath)) {
|
|
365
|
+
throw new Error(`Trace bundle not found: ${tracePath}`);
|
|
366
|
+
}
|
|
367
|
+
const verbose = input.verbose ?? false;
|
|
368
|
+
// Step 1: TOC.
|
|
369
|
+
const inspection = await inspectTrace({ tracePath });
|
|
370
|
+
// Step 2: chain analyzers in parallel. Each branch is fault-tolerant
|
|
371
|
+
// via buildAreaSummary so one failure doesn't tank the whole summary.
|
|
372
|
+
const [hangs, hitches, timeProfile, allocations, appLaunch] = await Promise.all([
|
|
373
|
+
buildAreaSummary("potential-hangs", inspection, () => analyzeHangs({
|
|
374
|
+
tracePath,
|
|
375
|
+
topN: DEFAULT_TOP_N_HANGS,
|
|
376
|
+
minDurationMs: DEFAULT_HANG_MIN_MS,
|
|
377
|
+
includeStackClassification: true,
|
|
378
|
+
}), "potential-hangs schema absent from this trace."),
|
|
379
|
+
buildAreaSummary("animation-hitches", inspection, () => analyzeAnimationHitches({
|
|
380
|
+
tracePath,
|
|
381
|
+
topN: DEFAULT_TOP_N_HITCHES,
|
|
382
|
+
minDurationMs: DEFAULT_HITCH_THRESHOLD_MS,
|
|
383
|
+
}), "animation-hitches schema absent from this trace."),
|
|
384
|
+
buildAreaSummary("time-profile", inspection, () => analyzeTimeProfile({
|
|
385
|
+
tracePath,
|
|
386
|
+
topN: DEFAULT_TOP_N_TIME_PROFILE,
|
|
387
|
+
}), "time-profile schema absent from this trace."),
|
|
388
|
+
buildAreaSummary("allocations", inspection, () => analyzeAllocations({
|
|
389
|
+
tracePath,
|
|
390
|
+
topN: DEFAULT_TOP_N_ALLOCATIONS,
|
|
391
|
+
minBytes: 0,
|
|
392
|
+
}), "allocations schema absent from this trace."),
|
|
393
|
+
buildAreaSummary("app-launch", inspection, () => analyzeAppLaunch({ tracePath }), "app-launch schema absent from this trace."),
|
|
394
|
+
]);
|
|
395
|
+
const areas = {
|
|
396
|
+
hangs,
|
|
397
|
+
hitches,
|
|
398
|
+
timeProfile,
|
|
399
|
+
allocations,
|
|
400
|
+
appLaunch,
|
|
401
|
+
};
|
|
402
|
+
const correlations = buildCorrelations(areas);
|
|
403
|
+
const headline = buildHeadline(areas);
|
|
404
|
+
const base = {
|
|
405
|
+
ok: true,
|
|
406
|
+
tracePath,
|
|
407
|
+
inspection,
|
|
408
|
+
areas,
|
|
409
|
+
correlations,
|
|
410
|
+
headline,
|
|
411
|
+
};
|
|
412
|
+
const markdown = buildMarkdownCard(base, verbose);
|
|
413
|
+
return { ...base, markdown };
|
|
414
|
+
}
|
|
415
|
+
// Helper for tests: ensure the keys in `areas` stay aligned with the
|
|
416
|
+
// implementation. Imported by the test file.
|
|
417
|
+
export const SUMMARIZE_AREA_KEYS = [
|
|
418
|
+
"hangs",
|
|
419
|
+
"hitches",
|
|
420
|
+
"timeProfile",
|
|
421
|
+
"allocations",
|
|
422
|
+
"appLaunch",
|
|
423
|
+
];
|
|
424
|
+
//# sourceMappingURL=summarizeTrace.js.map
|