ui: Improve Memscope Java and native attribution

Rank Java direct-footprint classes by shallow plus native size. Prefer app
frames in native allocation stacks, while filtering allocator plumbing and
falling back to meaningful framework or library callers.
diff --git a/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/java_section.ts b/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/java_section.ts
index 5580468..da7c5d3 100644
--- a/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/java_section.ts
+++ b/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/java_section.ts
@@ -70,7 +70,7 @@
   dominatedSizeBytes: number;
   dominatedNativeSizeBytes: number;
   rnRetained: number; // by retained (dominated Java + native)
-  rnShallow: number; // by shallow (reachable self)
+  rnShallow: number; // by direct footprint (reachable shallow + native)
   rnCount: number; // by reachable instance count
 }
 
@@ -186,7 +186,7 @@
         ) AS rn_retained,
         ROW_NUMBER() OVER (
           PARTITION BY a.graph_sample_ts
-          ORDER BY a.reachable_size_bytes DESC
+          ORDER BY a.reachable_size_bytes + a.reachable_native_size_bytes DESC
         ) AS rn_shallow,
         ROW_NUMBER() OVER (
           PARTITION BY a.graph_sample_ts ORDER BY a.reachable_obj_count DESC
@@ -626,9 +626,9 @@
       .map((c) =>
         classRow(
           c,
-          (r) => r.reachableSizeBytes,
-          cur.reachableHeapSize,
-          comparing ? baseStats.reachableHeapSize : 0,
+          (r) => r.reachableSizeBytes + r.reachableNativeSizeBytes,
+          reachableTotal,
+          baseReachableTotal,
         ),
       );
 
@@ -784,7 +784,7 @@
               }),
             shallowRows.length > 0 &&
               topTable({
-                title: 'Top classes by shallow size',
+                title: 'Top classes by shallow + native size',
                 cols: classCols,
                 rows: shallowRows,
               }),
diff --git a/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/native_section.ts b/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/native_section.ts
index 4cf558e..d3b0944 100644
--- a/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/native_section.ts
+++ b/ui/src/plugins/dev.perfetto.Memscope/views/landing_page/summary/native_section.ts
@@ -44,12 +44,14 @@
   "Aggregated by allocation callstack — the clearest signal when it's " +
   'available.';
 
-// One top-unreleased heapprofd callsite with its app-only frame chain
-// (leaf → root).
+// One top-unreleased heapprofd callsite. App-owned frames are kept as the
+// preferred snippet; when there are none, fallbackFrame is the nearest useful
+// framework/library caller above the allocator plumbing.
 interface NativeStack {
   unreleased: number;
   allocs: number;
   frames: string[];
+  fallbackFrame?: string;
 }
 
 interface NativeData {
@@ -179,7 +181,8 @@
 }
 
 // Loads the top native allocation call-stacks for one process over the window
-// (fromTs, toTs]. Frame chains are filtered to app frames.
+// (fromTs, toTs]. Prefer app frames, but retain the nearest meaningful
+// framework/library frame as a fallback when a stack has no app frames.
 async function loadNativeStacks(
   trace: Trace,
   upid: number,
@@ -262,9 +265,12 @@
       };
       stacks.push(cur);
     }
-    // Keep only frames from the app under test; drop system/runtime libraries.
-    if (isAppMapping(it.mapping_name ?? undefined)) {
-      cur.frames.push(it.frame_name);
+    if (isAllocatorPlumbing(it.frame_name)) continue;
+    const mapping = it.mapping_name ?? undefined;
+    if (isAppMapping(mapping)) {
+      cur.frames.push(frameLabel(it.frame_name, mapping));
+    } else if (cur.fallbackFrame === undefined) {
+      cur.fallbackFrame = frameLabel(it.frame_name, mapping);
     }
   }
   stacks.sort((a, b) => b.unreleased - a.unreleased);
@@ -304,9 +310,37 @@
   return /^\/data\/(app|data)\//.test(mapping);
 }
 
-// Shrinks an (already app-only) frame chain (leaf → root) to a short snippet.
-function stackSnippet(frames: string[]): string[] {
-  if (frames.length === 0) return ['(no app frames)'];
+// Removes low-level allocation entry points which identify how memory was
+// allocated, but not which component requested it.
+function isAllocatorPlumbing(name: string): boolean {
+  const n = name.toLowerCase();
+  return (
+    /(^|::|__)(malloc|calloc|realloc|memalign|aligned_alloc|posix_memalign)(@.*|\(.*\))?$/.test(
+      n,
+    ) ||
+    /(^|::)operator new(\[\])?(\(.*\))?$/.test(n) ||
+    n.includes('malloc_hook') ||
+    n.includes('heapprofd') ||
+    n.includes('scudo::') ||
+    n.includes('jemalloc') ||
+    /^je_(malloc|calloc|realloc|memalign)/.test(n)
+  );
+}
+
+// Prefix non-app frames with their binary name so a framework/library fallback
+// remains useful even when its function name is ambiguous or unavailable.
+function frameLabel(name: string, mapping?: string): string {
+  if (mapping === undefined || isAppMapping(mapping)) return name;
+  const binary = mapping.slice(mapping.lastIndexOf('/') + 1);
+  return name === '<unknown>' ? binary : `${binary} · ${name}`;
+}
+
+// Shrinks an app frame chain (leaf → root) to a short snippet, or shows the
+// nearest meaningful framework/library caller when no app frame was captured.
+function stackSnippet(frames: string[], fallbackFrame?: string): string[] {
+  if (frames.length === 0) {
+    return [fallbackFrame ?? '(allocation origin unresolved)'];
+  }
   if (frames.length <= 7) return frames;
   const leaf = frames[0];
   const middle = frames.slice(1, -2).slice(0, 4);
@@ -365,12 +399,8 @@
     const stackRows = data.stacks.slice(0, 5).map((s) => [
       m(
         '.pf-memscope-stack',
-        stackSnippet(s.frames).map((f) =>
-          m(
-            '.pf-memscope-stack__frame',
-            {title: f},
-            f.length > 60 ? `${f.slice(0, 58)}…` : f,
-          ),
+        stackSnippet(s.frames, s.fallbackFrame).map((f) =>
+          m('.pf-memscope-stack__frame', {title: f}, f),
         ),
       ),
       formatBytes(s.unreleased),
@@ -463,10 +493,11 @@
                 title: 'Top allocation call-stacks',
                 subtitle: comparing
                   ? 'unreleased between baseline and selected snapshot · ' +
-                    'app frames only'
+                    'app frames preferred'
                   : selTs !== undefined
-                    ? 'unreleased up to the selected snapshot · app frames only'
-                    : 'unreleased over the whole trace · app frames only',
+                    ? 'unreleased up to the selected snapshot · ' +
+                      'app frames preferred'
+                    : 'unreleased over the whole trace · app frames preferred',
                 cols: [
                   {label: 'Call-stack snippet'},
                   {label: comparing ? 'Δ unreleased' : 'Unreleased', num: true},
@@ -478,8 +509,8 @@
             : m(
                 '.pf-memscope-placeholder',
                 comparing
-                  ? 'No app allocations captured between these snapshots.'
-                  : 'No app allocations captured by the profiler here.',
+                  ? 'No unreleased allocations between these snapshots.'
+                  : 'No unreleased allocations captured by the profiler here.',
               ),
         ]),
       ),