@@ -65,6 +65,7 @@ const SHORT_KEY_TO_TRANSFORM: { [string]: TransformType } = {};
65
65
'focus-category' ,
66
66
'merge-call-node' ,
67
67
'merge-function' ,
68
+ 'merge-unaccounted-native-functions' ,
68
69
'drop-function' ,
69
70
'collapse-resource' ,
70
71
'collapse-direct-recursion' ,
@@ -91,6 +92,9 @@ const SHORT_KEY_TO_TRANSFORM: { [string]: TransformType } = {};
91
92
case 'merge-function' :
92
93
shortKey = 'mf' ;
93
94
break ;
95
+ case 'merge-unaccounted-native-functions' :
96
+ shortKey = 'munfs' ;
97
+ break ;
94
98
case 'drop-function' :
95
99
shortKey = 'df' ;
96
100
break ;
@@ -235,6 +239,12 @@ export function parseTransforms(transformString: string): TransformStack {
235
239
}
236
240
break ;
237
241
}
242
+ case 'merge-unaccounted-native-functions' : {
243
+ transforms . push ( {
244
+ type : 'merge-unaccounted-native-functions' ,
245
+ } ) ;
246
+ break ;
247
+ }
238
248
case 'focus-category' : {
239
249
// e.g. "fg-3"
240
250
const [ , categoryRaw ] = tuple ;
@@ -348,6 +358,8 @@ export function stringifyTransforms(transformStack: TransformStack): string {
348
358
case 'collapse-function-subtree' :
349
359
case 'focus-function' :
350
360
return `${ shortKey } -${ transform . funcIndex } ` ;
361
+ case 'merge-unaccounted-native-functions' :
362
+ return shortKey ;
351
363
case 'focus-category' :
352
364
return `${ shortKey } -${ transform . category } ` ;
353
365
case 'collapse-resource' :
@@ -432,6 +444,13 @@ export function getTransformLabelL10nIds(
432
444
}
433
445
}
434
446
447
+ if ( transform . type === 'merge-unaccounted-native-functions' ) {
448
+ return {
449
+ l10nId : 'TransformNavigator--merge-unaccounted-native-functions' ,
450
+ item : '' ,
451
+ } ;
452
+ }
453
+
435
454
// Lookup function name.
436
455
let funcIndex ;
437
456
switch ( transform . type ) {
@@ -517,6 +536,12 @@ export function applyTransformToCallNodePath(
517
536
return _mergeNodeInCallNodePath ( transform . callNodePath , callNodePath ) ;
518
537
case 'merge-function' :
519
538
return _mergeFunctionInCallNodePath ( transform . funcIndex , callNodePath ) ;
539
+ case 'merge-unaccounted-native-functions' :
540
+ return _mergeUnaccountedNativeFunctionsInCallNodePath (
541
+ callNodePath ,
542
+ transformedThread . funcTable ,
543
+ transformedThread . stringTable
544
+ ) ;
520
545
case 'drop-function' :
521
546
return _dropFunctionInCallNodePath ( transform . funcIndex , callNodePath ) ;
522
547
case 'collapse-resource' :
@@ -588,6 +613,38 @@ function _mergeFunctionInCallNodePath(
588
613
return callNodePath . filter ( ( nodeFunc ) => nodeFunc !== funcIndex ) ;
589
614
}
590
615
616
+ function _mergeUnaccountedNativeFunctionsInCallNodePath (
617
+ callNodePath : CallNodePath ,
618
+ funcTable : FuncTable ,
619
+ stringTable : UniqueStringArray
620
+ ) : CallNodePath {
621
+ return callNodePath . filter ( ( nodeFunc ) =>
622
+ isUnaccountedNativeFunction ( nodeFunc , funcTable , stringTable )
623
+ ) ;
624
+ }
625
+
626
+ // Returns true if funcIndex is probably a JIT frame outside of any known JIT
627
+ // address mappings.
628
+ export function isUnaccountedNativeFunction (
629
+ funcIndex : IndexIntoFuncTable ,
630
+ funcTable : FuncTable ,
631
+ stringTable : UniqueStringArray
632
+ ) : boolean {
633
+ if ( funcTable . resource [ funcIndex ] !== - 1 ) {
634
+ // This function has a resource. That means it's not "unaccounted".
635
+ return false ;
636
+ }
637
+ if ( funcTable . isJS [ funcIndex ] ) {
638
+ // This function is a JS function. That means it's not a "native" function.
639
+ return false ;
640
+ }
641
+ // Ok, so now we either have a native function without a library (otherwise it
642
+ // would have a "lib" resource), or we have a label frame. Assume that label
643
+ // frames don't start with "0x".
644
+ const locationString = stringTable . getString ( funcTable . name [ funcIndex ] ) ;
645
+ return locationString . startsWith ( '0x' ) ;
646
+ }
647
+
591
648
function _dropFunctionInCallNodePath (
592
649
funcIndex : IndexIntoFuncTable ,
593
650
callNodePath : CallNodePath
@@ -859,13 +916,96 @@ export function mergeFunction(
859
916
} ) ;
860
917
}
861
918
919
+ /**
920
+ * Returns a Uint8Array filled with zeros and ones.
921
+ * result[funcIndex] === 1 iff isUnaccountedNativeFunction(funcIndex)
922
+ */
923
+ function getUnaccountedNativeFunctions ( thread : Thread ) : Uint8Array {
924
+ const { funcTable, stringTable } = thread ;
925
+ const funcCount = funcTable . length ;
926
+ const { isJS : funcIsJS , resource : funcResource , name : funcName } = funcTable ;
927
+ const isUnaccountedNativeFunctionArr = new Uint8Array ( funcTable . length ) ;
928
+ for ( let i = 0 ; i < funcCount ; i ++ ) {
929
+ if ( funcIsJS [ i ] || funcResource [ i ] !== - 1 ) {
930
+ continue ;
931
+ }
932
+ const locationString = stringTable . getString ( funcName [ i ] ) ;
933
+ if ( locationString . startsWith ( '0x' ) ) {
934
+ isUnaccountedNativeFunctionArr [ i ] = 1 ;
935
+ }
936
+ }
937
+ return isUnaccountedNativeFunctionArr ;
938
+ }
939
+
940
+ export function mergeUnaccountedNativeFunctions ( thread : Thread ) : Thread {
941
+ const isUnaccountedNativeFunctionArr = getUnaccountedNativeFunctions ( thread ) ;
942
+ return mergeFunctions ( thread , isUnaccountedNativeFunctionArr ) ;
943
+ }
944
+
945
+ export function mergeFunctions (
946
+ thread : Thread ,
947
+ shouldMergeFunction : Uint8Array
948
+ ) : Thread {
949
+ const { stackTable, frameTable } = thread ;
950
+
951
+ // A map oldStack -> newStack+1, implemented as a Uint32Array for performance.
952
+ // If newStack+1 is zero it means "null", i.e. this stack was filtered out.
953
+ // Typed arrays are initialized to zero, which we interpret as null.
954
+ //
955
+ // For each old stack, the new stack is computed as follows:
956
+ // - If the old stack's function is not funcIndexToMerge, then the new stack
957
+ // is the same as the old stack.
958
+ // - If the old stack's function is funcIndexToMerge, then the new stack is
959
+ // the closest ancestor whose func is not funcIndexToMerge, or null if no
960
+ // such ancestor exists.
961
+ //
962
+ // We only compute a new prefix column; the other columns are copied from the
963
+ // old stack table. The skipped stacks are "orphaned"; they'll still be present
964
+ // in the new stack table but not referenced by samples or other stacks.
965
+ const oldStackToNewStackPlusOne = new Uint32Array ( stackTable . length ) ;
966
+
967
+ const stackTableFrameCol = stackTable . frame ;
968
+ const frameTableFuncCol = frameTable . func ;
969
+ const oldPrefixCol = stackTable . prefix ;
970
+ const newPrefixCol = new Array ( stackTable . length ) ;
971
+
972
+ for ( let stackIndex = 0 ; stackIndex < stackTable . length ; stackIndex ++ ) {
973
+ const oldPrefix = oldPrefixCol [ stackIndex ] ;
974
+ const newPrefixPlusOne =
975
+ oldPrefix === null ? 0 : oldStackToNewStackPlusOne [ oldPrefix ] ;
976
+
977
+ const frameIndex = stackTableFrameCol [ stackIndex ] ;
978
+ const funcIndex = frameTableFuncCol [ frameIndex ] ;
979
+ if ( shouldMergeFunction [ funcIndex ] === 1 ) {
980
+ oldStackToNewStackPlusOne [ stackIndex ] = newPrefixPlusOne ;
981
+ } else {
982
+ oldStackToNewStackPlusOne [ stackIndex ] = stackIndex + 1 ;
983
+ }
984
+ const newPrefix = newPrefixPlusOne === 0 ? null : newPrefixPlusOne - 1 ;
985
+ newPrefixCol [ stackIndex ] = newPrefix ;
986
+ }
987
+
988
+ const newStackTable = {
989
+ ...stackTable ,
990
+ prefix : newPrefixCol ,
991
+ } ;
992
+
993
+ return updateThreadStacks ( thread , newStackTable , ( oldStack ) => {
994
+ if ( oldStack === null ) {
995
+ return null ;
996
+ }
997
+ const newStackPlusOne = oldStackToNewStackPlusOne [ oldStack ] ;
998
+ return newStackPlusOne === 0 ? null : newStackPlusOne - 1 ;
999
+ } ) ;
1000
+ }
1001
+
862
1002
/**
863
1003
* Drop any samples that contain the given function.
864
1004
*/
865
1005
export function dropFunction (
866
1006
thread : Thread ,
867
1007
funcIndexToDrop : IndexIntoFuncTable
868
- ) {
1008
+ ) : Thread {
869
1009
const { stackTable, frameTable } = thread ;
870
1010
871
1011
// Go through each stack, and label it as containing the function or not.
@@ -1774,6 +1914,8 @@ export function applyTransform(
1774
1914
) ;
1775
1915
case 'merge-function' :
1776
1916
return mergeFunction ( thread , transform . funcIndex ) ;
1917
+ case 'merge-unaccounted-native-functions' :
1918
+ return mergeUnaccountedNativeFunctions ( thread ) ;
1777
1919
case 'drop-function' :
1778
1920
return dropFunction ( thread , transform . funcIndex ) ;
1779
1921
case 'focus-function' :
0 commit comments