You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Summary:
Three correctness/effectiveness improvements to the memlab MCP server. Source of truth is `www/scripts/webspeed/memory_lab/packages/mcp-server/`; the `fbcode/claude-templates/.../memlab-server/src/` copy is synced via the sync script.
**1. `auto_investigate` "Same-Object Duplication" reported a wildly wrong copy factor (correctness).** The duplication factor was computed as `sampleCount / cardinality` over *any* unique-looking field, so a CONSTANT field (e.g. `gala_project_id` with 1 distinct value) reported "~200 retained copies" when the real duplication was 2×. The over-stated magnitude is misleading and (in one case) propagated into a diff summary before being caught. Rewrote the per-shape analysis into two passes that derive the factor from the MOST-DISTINCT string-valued field — the record's true discriminator — instead of an arbitrary `*_id`-shaped key. Now 2 real copies read as `≈2×`, genuinely-distinct records produce no false positive (the most-distinct field is ~unique), and the alert is phrased as a sample-based estimate with a "confirm with `memlab_property_distribution`" pointer.
**2. `sliced_strings` was blind to the "many medium parents" case (effectiveness).** When `min_parent_size` filtered out every parent (e.g. thousands of ~250 KB JSON records, each sub-MB), the tool returned only counts and no actionable output. Added a `**Total parent bytes pinned by slices:**` aggregate (Σ distinct parent self-sizes) reported regardless of the filter, and a fallback that shows the largest parents (with a note) instead of going silent when nothing meets `min_parent_size`.
**3. `eval` `helpers.retainedSizes()` return type was undocumented (DX).** It returns a `Record<id, bytes>` object, not an array; the docs implied an array, leading to `sizes.reduce is not a function`. Documented the object return shape (iterate with `Object.values(sizes)` / index `sizes[id]`) in the `describe_env` output, the tool description, and the agent-facing `SKILL.md`.
Versions bumped (the 4 independently-versioned spots): OSS `mcp-server/package.json` 2.5.0 → 2.6.0, the hardcoded MCP handshake version literal in `src/index.ts` 2.5.0 → 2.6.0 (synced to the plugin), plugin `memlab-server/package.json` 2.5.0 → 2.6.0, and plugin `.claude-plugin/plugin.json` 1.12.0 → 1.13.0.
___
Differential Revision: D108944208
fbshipit-source-id: 9f86231b0062c4410eaff2737dff108bee56aeab
// derive the factor from the MOST-DISTINCT string-valued field — the
1949
+
// record's true discriminator — NOT from an arbitrary unique-looking
1950
+
// field. The old code did `sampleCount / cardinality` on any
1951
+
// `*_id`-shaped key, so a CONSTANT field (e.g. `gala_project_id`
1952
+
// with 1 value) reported "~200 copies" when the real duplication was
1953
+
// 2×. If even the most-distinct field repeats, the whole record is
1954
+
// duplicated; if the most-distinct field is ~unique, there is no
1955
+
// duplication regardless of how many constant columns exist.
1956
+
conststringStats=propStats.filter(
1957
+
p=>p.stringValueCount>=sampleCount*0.5,
1958
+
);
1959
+
letmaxCard=0;
1960
+
letdiscrim='';
1961
+
for(constpofstringStats){
1962
+
if(p.cardinality>maxCard){
1963
+
maxCard=p.cardinality;
1964
+
discrim=p.name;
1965
+
}
1966
+
}
1967
+
if(
1968
+
stringStats.length>0&&
1969
+
maxCard>=1&&
1970
+
maxCard<sampleCount*0.6&&
1971
+
sampleCount>=100&&
1972
+
!dupShapesSeen.has(shapeKey)
1973
+
){
1974
+
constcopyFactor=Math.round(sampleCount/maxCard);
1975
+
if(copyFactor>=2){
1976
+
dupShapesSeen.add(shapeKey);
1977
+
duplicationAlerts.push(
1978
+
`- \`{${s.properties.slice(0,4).join(', ')}${s.properties.length>4 ? ', …' : ''}}\` (${formatNumber(s.count)} total instances): even the most-distinct field \`${discrim}\` has only **${formatNumber(maxCard)} distinct value(s)** across ${formatNumber(sampleCount)} sampled — the same records appear to be retained **≈${copyFactor}×** (sample-based estimate; confirm the exact factor with \`memlab_property_distribution\` on \`${discrim}\`, or a content-hash count). Investigate **retention/concurrency** (e.g. many concurrent requests each holding a full copy of this dataset, or the same listing materialized twice), NOT string interning.`,
1946
1979
);
1947
-
if(dupFactor>=2){
1948
-
dupShapesSeen.add(shapeKey);
1949
-
duplicationAlerts.push(
1950
-
`- \`${propName}\` (a unique-looking key) has only **${formatNumber(cardinality)} distinct value(s)** across ${formatNumber(sampleCount)} sampled of ${formatNumber(s.count)} total \`{${s.properties.slice(0,4).join(', ')}${s.properties.length>4 ? ', …' : ''}}\` instances — looks like **~${dupFactor} retained copies of the same records**, not a low-cardinality column. Investigate **retention/concurrency** (e.g. many concurrent requests each holding a full copy of this dataset), NOT string interning.`,
? `- Property \`${propName}\` has only **${cardinality} unique value(s)** across ${formatNumber(s.count)} instances — low-cardinality column suitable for pre-filtering at the data source or string interning`
1966
-
: `- Property \`${propName}\` has only **${cardinality} unique value(s)** across ${formatNumber(s.count)} instances — low-cardinality column suitable for pre-filtering at the data source`,
1994
+
? `- Property \`${p.name}\` has only **${p.cardinality} unique value(s)** across ${formatNumber(s.count)} instances — low-cardinality column suitable for pre-filtering at the data source or string interning`
1995
+
: `- Property \`${p.name}\` has only **${p.cardinality} unique value(s)** across ${formatNumber(s.count)} instances — low-cardinality column suitable for pre-filtering at the data source`,
1967
1996
);
1968
1997
}
1969
1998
1970
-
// High-cost unique: all values unique, string, large average size
1999
+
// High-cost unique: all values unique, string, large average size.
1971
2000
if(
1972
-
allUnique&&
1973
-
stringValueCount>sampleCount*0.8&&
1974
-
cardinality>sampleCount*0.9
2001
+
p.allUnique&&
2002
+
p.stringValueCount>sampleCount*0.8&&
2003
+
p.cardinality>sampleCount*0.9
1975
2004
){
1976
-
constavgSize=totalValueSize/stringValueCount;
2005
+
constavgSize=p.totalValueSize/p.stringValueCount;
1977
2006
if(avgSize>50&&s.count>10_000){
1978
2007
constestimatedWaste=avgSize*s.count;
1979
2008
columnAlerts.push(
1980
-
`- Property \`${propName}\` has **all unique string values** (avg ${Math.round(avgSize)}B each × ${formatNumber(s.count)} instances = ~${formatBytes(estimatedWaste)}) — verify this field is needed by consumers`,
2009
+
`- Property \`${p.name}\` has **all unique string values** (avg ${Math.round(avgSize)}B each × ${formatNumber(s.count)} instances = ~${formatBytes(estimatedWaste)}) — verify this field is needed by consumers`,
Copy file name to clipboardExpand all lines: packages/mcp-server/src/tools/eval.ts
+4-4Lines changed: 4 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -142,7 +142,7 @@ export function registerEval(server: McpServer): void {
142
142
'**Iterating all nodes:** `snapshot.nodes.forEach(node => { ... })` — NOT for-of.\n'+
143
143
'**Get node by ID:** `snapshot.getNodeById(id)` returns IHeapNode or null.\n'+
144
144
'**String values:** `node.toStringNode()?.stringValue` for string nodes.\n'+
145
-
'**Caveat — retained_size is unreliable here:** inside eval, `node.retained_size`/`.retainedSize` can read back ~0 for every node on some loads. Node counts, property/edge walks, and string values ARE trustworthy. For authoritative retained sizes call `helpers.retainedSize(id)` / `helpers.retainedSizes([ids])` (they re-resolve the node on the real snapshot), or use the dedicated tools (`memlab_largest_objects`, `memlab_class_histogram`, `memlab_pinch_points`, `memlab_object_shape`).\n\n'+
145
+
'**Caveat — retained_size is unreliable here:** inside eval, `node.retained_size`/`.retainedSize` can read back ~0 for every node on some loads. Node counts, property/edge walks, and string values ARE trustworthy. For authoritative retained sizes call `helpers.retainedSize(id)` (number) / `helpers.retainedSizes([ids])` (a `Record<id, bytes>` object, NOT an array) — they re-resolve the node on the real snapshot — or use the dedicated tools (`memlab_largest_objects`, `memlab_class_histogram`, `memlab_pinch_points`, `memlab_object_shape`).\n\n'+
'retainedSize(id)->number, retainedSizes(ids[])->Record<id,bytes> (an OBJECT keyed by id, NOT an array — index it as sizes[id] or Object.values(sizes)) }), '+
173
173
'and standard JS built-ins. '+
174
174
'Node traversal: use node.references (outgoing) and node.referrers (incoming) with for-of. '+
'Inside eval, `.retainedSize`/`.retained_size` can read back ~0 for every node on some loads. Counts, property/edge walks, and string values are reliable. For authoritative retained sizes call `helpers.retainedSize(id)` or `helpers.retainedSizes([ids])` (they re-resolve the node on the real snapshot, so you can rank custom analyses by retained size), or use `memlab_largest_objects`, `memlab_class_histogram`, `memlab_pinch_points`, or `memlab_object_shape`.',
447
+
'Inside eval, `.retainedSize`/`.retained_size` can read back ~0 for every node on some loads. Counts, property/edge walks, and string values are reliable. For authoritative retained sizes call `helpers.retainedSize(id)` (returns a number) or `helpers.retainedSizes([ids])` (returns a `Record<id, bytes>` OBJECT — not an array; iterate with `Object.values(sizes)` / index with `sizes[id]`) — both re-resolve the node on the real snapshot, so you can rank custom analyses by retained size. Or use `memlab_largest_objects`, `memlab_class_histogram`, `memlab_pinch_points`, or `memlab_object_shape`.',
`**Total parent bytes pinned by slices:** ${formatBytes(totalPinnedBytes)} across ${formatNumber(allParents.length)} parent string(s) — substrings keep these alive even when each individual parent is small.`,
155
+
);
156
+
}
132
157
lines.push('');
133
158
134
159
if(topParents.length>0){
@@ -149,7 +174,9 @@ export function registerSlicedStrings(server: McpServer): void {
149
174
formatNumber(p.sliced_ref_count),
150
175
formatBytes(p.sliced_total_size),
151
176
]);
152
-
lines.push('### Top parent strings (keeping slices alive)');
177
+
lines.push(
178
+
`### Top parent strings (keeping slices alive)${filteredNote}`,
0 commit comments