TracingModel.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. export default class TracingModel{constructor(backingStorage){this._backingStorage=backingStorage;this._firstWritePending=true;this._processById=new Map();this._processByName=new Map();this._minimumRecordTime=0;this._maximumRecordTime=0;this._devToolsMetadataEvents=[];this._asyncEvents=[];this._openAsyncEvents=new Map();this._openNestableAsyncEvents=new Map();this._profileGroups=new Map();this._parsedCategories=new Map();}
  2. static isNestableAsyncPhase(phase){return phase==='b'||phase==='e'||phase==='n';}
  3. static isAsyncBeginPhase(phase){return phase==='S'||phase==='b';}
  4. static isAsyncPhase(phase){return TracingModel.isNestableAsyncPhase(phase)||phase==='S'||phase==='T'||phase==='F'||phase==='p';}
  5. static isFlowPhase(phase){return phase==='s'||phase==='t'||phase==='f';}
  6. static isTopLevelEvent(event){return event.hasCategory(DevToolsTimelineEventCategory)&&event.name==='RunTask'||event.hasCategory(LegacyTopLevelEventCategory)||event.hasCategory(DevToolsMetadataEventCategory)&&event.name==='Program';}
  7. static _extractId(payload){const scope=payload.scope||'';if(typeof payload.id2==='undefined'){return scope&&payload.id?`${scope}@${payload.id}`:payload.id;}
  8. const id2=payload.id2;if(typeof id2==='object'&&('global'in id2)!==('local'in id2)){return typeof id2['global']!=='undefined'?`:${scope}:${id2['global']}`:`:${scope}:${payload.pid}:${id2['local']}`;}
  9. console.error(`Unexpected id2 field at ${payload.ts / 1000}, one and only one of 'local' and 'global' should be present.`);}
  10. static browserMainThread(tracingModel){const processes=tracingModel.sortedProcesses();if(!processes.length){return null;}
  11. const browserMainThreadName='CrBrowserMain';const browserProcesses=[];const browserMainThreads=[];for(const process of processes){if(process.name().toLowerCase().endsWith('browser')){browserProcesses.push(process);}
  12. browserMainThreads.push(...process.sortedThreads().filter(t=>t.name()===browserMainThreadName));}
  13. if(browserMainThreads.length===1){return browserMainThreads[0];}
  14. if(browserProcesses.length===1){return browserProcesses[0].threadByName(browserMainThreadName);}
  15. const tracingStartedInBrowser=tracingModel.devToolsMetadataEvents().filter(e=>e.name==='TracingStartedInBrowser');if(tracingStartedInBrowser.length===1){return tracingStartedInBrowser[0].thread;}
  16. Common.console.error('Failed to find browser main thread in trace, some timeline features may be unavailable');return null;}
  17. devToolsMetadataEvents(){return this._devToolsMetadataEvents;}
  18. addEvents(events){for(let i=0;i<events.length;++i){this._addEvent(events[i]);}}
  19. tracingComplete(){this._processPendingAsyncEvents();this._backingStorage.appendString(this._firstWritePending?'[]':']');this._backingStorage.finishWriting();this._firstWritePending=false;for(const process of this._processById.values()){for(const thread of process._threads.values()){thread.tracingComplete();}}}
  20. dispose(){if(!this._firstWritePending){this._backingStorage.reset();}}
  21. adjustTime(offset){this._minimumRecordTime+=offset;this._maximumRecordTime+=offset;for(const process of this._processById.values()){for(const thread of process._threads.values()){for(const event of thread.events()){event.startTime+=offset;if(typeof event.endTime==='number'){event.endTime+=offset;}}
  22. for(const event of thread.asyncEvents()){event.startTime+=offset;if(typeof event.endTime==='number'){event.endTime+=offset;}}}}}
  23. _addEvent(payload){let process=this._processById.get(payload.pid);if(!process){process=new Process(this,payload.pid);this._processById.set(payload.pid,process);}
  24. const phase=Phase;const eventsDelimiter=',\n';this._backingStorage.appendString(this._firstWritePending?'[':eventsDelimiter);this._firstWritePending=false;const stringPayload=JSON.stringify(payload);const isAccessible=payload.ph===phase.SnapshotObject;let backingStorage=null;const keepStringsLessThan=10000;if(isAccessible&&stringPayload.length>keepStringsLessThan){backingStorage=this._backingStorage.appendAccessibleString(stringPayload);}else{this._backingStorage.appendString(stringPayload);}
  25. const timestamp=payload.ts/1000;if(timestamp&&(!this._minimumRecordTime||timestamp<this._minimumRecordTime)&&(payload.ph===phase.Begin||payload.ph===phase.Complete||payload.ph===phase.Instant)){this._minimumRecordTime=timestamp;}
  26. const endTimeStamp=(payload.ts+(payload.dur||0))/1000;this._maximumRecordTime=Math.max(this._maximumRecordTime,endTimeStamp);const event=process._addEvent(payload);if(!event){return;}
  27. if(payload.ph===phase.Sample){this._addSampleEvent(event);return;}
  28. if(TracingModel.isAsyncPhase(payload.ph)){this._asyncEvents.push(event);}
  29. event._setBackingStorage(backingStorage);if(event.hasCategory(DevToolsMetadataEventCategory)){this._devToolsMetadataEvents.push(event);}
  30. if(payload.ph!==phase.Metadata){return;}
  31. switch(payload.name){case MetadataEvent.ProcessSortIndex:process._setSortIndex(payload.args['sort_index']);break;case MetadataEvent.ProcessName:const processName=payload.args['name'];process._setName(processName);this._processByName.set(processName,process);break;case MetadataEvent.ThreadSortIndex:process.threadById(payload.tid)._setSortIndex(payload.args['sort_index']);break;case MetadataEvent.ThreadName:process.threadById(payload.tid)._setName(payload.args['name']);break;}}
  32. _addSampleEvent(event){const id=`${event.thread.process().id()}:${event.id}`;const group=this._profileGroups.get(id);if(group){group._addChild(event);}else{this._profileGroups.set(id,new ProfileEventsGroup(event));}}
  33. profileGroup(event){return this._profileGroups.get(`${event.thread.process().id()}:${event.id}`)||null;}
  34. minimumRecordTime(){return this._minimumRecordTime;}
  35. maximumRecordTime(){return this._maximumRecordTime;}
  36. sortedProcesses(){return NamedObject._sort(this._processById.valuesArray());}
  37. processByName(name){return this._processByName.get(name);}
  38. processById(pid){return this._processById.get(pid)||null;}
  39. threadByName(processName,threadName){const process=this.processByName(processName);return process&&process.threadByName(threadName);}
  40. _processPendingAsyncEvents(){this._asyncEvents.sort(Event.compareStartTime);for(let i=0;i<this._asyncEvents.length;++i){const event=this._asyncEvents[i];if(TracingModel.isNestableAsyncPhase(event.phase)){this._addNestableAsyncEvent(event);}else{this._addAsyncEvent(event);}}
  41. this._asyncEvents=[];this._closeOpenAsyncEvents();}
  42. _closeOpenAsyncEvents(){for(const event of this._openAsyncEvents.values()){event.setEndTime(this._maximumRecordTime);event.steps[0].setEndTime(this._maximumRecordTime);}
  43. this._openAsyncEvents.clear();for(const eventStack of this._openNestableAsyncEvents.values()){while(eventStack.length){eventStack.pop().setEndTime(this._maximumRecordTime);}}
  44. this._openNestableAsyncEvents.clear();}
  45. _addNestableAsyncEvent(event){const phase=Phase;const key=event.categoriesString+'.'+event.id;let openEventsStack=this._openNestableAsyncEvents.get(key);switch(event.phase){case phase.NestableAsyncBegin:if(!openEventsStack){openEventsStack=[];this._openNestableAsyncEvents.set(key,openEventsStack);}
  46. const asyncEvent=new AsyncEvent(event);openEventsStack.push(asyncEvent);event.thread._addAsyncEvent(asyncEvent);break;case phase.NestableAsyncInstant:if(openEventsStack&&openEventsStack.length){openEventsStack.peekLast()._addStep(event);}
  47. break;case phase.NestableAsyncEnd:if(!openEventsStack||!openEventsStack.length){break;}
  48. const top=openEventsStack.pop();if(top.name!==event.name){console.error(`Begin/end event mismatch for nestable async event, ${top.name} vs. ${event.name}, key: ${key}`);break;}
  49. top._addStep(event);}}
  50. _addAsyncEvent(event){const phase=Phase;const key=event.categoriesString+'.'+event.name+'.'+event.id;let asyncEvent=this._openAsyncEvents.get(key);if(event.phase===phase.AsyncBegin){if(asyncEvent){console.error(`Event ${event.name} has already been started`);return;}
  51. asyncEvent=new AsyncEvent(event);this._openAsyncEvents.set(key,asyncEvent);event.thread._addAsyncEvent(asyncEvent);return;}
  52. if(!asyncEvent){return;}
  53. if(event.phase===phase.AsyncEnd){asyncEvent._addStep(event);this._openAsyncEvents.delete(key);return;}
  54. if(event.phase===phase.AsyncStepInto||event.phase===phase.AsyncStepPast){const lastStep=asyncEvent.steps.peekLast();if(lastStep.phase!==phase.AsyncBegin&&lastStep.phase!==event.phase){console.assert(false,'Async event step phase mismatch: '+lastStep.phase+' at '+lastStep.startTime+' vs. '+
  55. event.phase+' at '+event.startTime);return;}
  56. asyncEvent._addStep(event);return;}
  57. console.assert(false,'Invalid async event phase');}
  58. backingStorage(){return this._backingStorage;}
  59. _parsedCategoriesForString(str){let parsedCategories=this._parsedCategories.get(str);if(!parsedCategories){parsedCategories=new Set(str?str.split(','):[]);this._parsedCategories.set(str,parsedCategories);}
  60. return parsedCategories;}}
  61. export const Phase={Begin:'B',End:'E',Complete:'X',Instant:'I',AsyncBegin:'S',AsyncStepInto:'T',AsyncStepPast:'p',AsyncEnd:'F',NestableAsyncBegin:'b',NestableAsyncEnd:'e',NestableAsyncInstant:'n',FlowBegin:'s',FlowStep:'t',FlowEnd:'f',Metadata:'M',Counter:'C',Sample:'P',CreateObject:'N',SnapshotObject:'O',DeleteObject:'D'};export const MetadataEvent={ProcessSortIndex:'process_sort_index',ProcessName:'process_name',ThreadSortIndex:'thread_sort_index',ThreadName:'thread_name'};export const LegacyTopLevelEventCategory='toplevel';export const DevToolsMetadataEventCategory='disabled-by-default-devtools.timeline';export const DevToolsTimelineEventCategory='disabled-by-default-devtools.timeline';export class BackingStorage{appendString(string){}
  62. appendAccessibleString(string){}
  63. finishWriting(){}
  64. reset(){}}
  65. export class Event{constructor(categories,name,phase,startTime,thread){this.categoriesString=categories||'';this._parsedCategories=thread._model._parsedCategoriesForString(this.categoriesString);this.name=name;this.phase=phase;this.startTime=startTime;this.thread=thread;this.args={};this.selfTime=0;}
  66. static fromPayload(payload,thread){const event=new Event(payload.cat,payload.name,(payload.ph),payload.ts/1000,thread);if(payload.args){event.addArgs(payload.args);}
  67. if(typeof payload.dur==='number'){event.setEndTime((payload.ts+payload.dur)/1000);}
  68. const id=TracingModel._extractId(payload);if(typeof id!=='undefined'){event.id=id;}
  69. if(payload.bind_id){event.bind_id=payload.bind_id;}
  70. return event;}
  71. static compareStartTime(a,b){return a.startTime-b.startTime;}
  72. static orderedCompareStartTime(a,b){return a.startTime-b.startTime||a.ordinal-b.ordinal||-1;}
  73. hasCategory(categoryName){return this._parsedCategories.has(categoryName);}
  74. setEndTime(endTime){if(endTime<this.startTime){console.assert(false,'Event out of order: '+this.name);return;}
  75. this.endTime=endTime;this.duration=endTime-this.startTime;}
  76. addArgs(args){for(const name in args){if(name in this.args){console.error('Same argument name ('+name+') is used for begin and end phases of '+this.name);}
  77. this.args[name]=args[name];}}
  78. _complete(endEvent){if(endEvent.args){this.addArgs(endEvent.args);}else{console.error('Missing mandatory event argument \'args\' at '+endEvent.startTime);}
  79. this.setEndTime(endEvent.startTime);}
  80. _setBackingStorage(backingStorage){}}
  81. export class ObjectSnapshot extends Event{constructor(category,name,startTime,thread){super(category,name,Phase.SnapshotObject,startTime,thread);this._backingStorage=null;this.id;this._objectPromise=null;}
  82. static fromPayload(payload,thread){const snapshot=new ObjectSnapshot(payload.cat,payload.name,payload.ts/1000,thread);const id=TracingModel._extractId(payload);if(typeof id!=='undefined'){snapshot.id=id;}
  83. if(!payload.args||!payload.args['snapshot']){console.error('Missing mandatory \'snapshot\' argument at '+payload.ts/1000);return snapshot;}
  84. if(payload.args){snapshot.addArgs(payload.args);}
  85. return snapshot;}
  86. requestObject(callback){const snapshot=this.args['snapshot'];if(snapshot){callback(snapshot);return;}
  87. this._backingStorage().then(onRead,callback.bind(null,null));function onRead(result){if(!result){callback(null);return;}
  88. try{const payload=JSON.parse(result);callback(payload['args']['snapshot']);}catch(e){Common.console.error('Malformed event data in backing storage');callback(null);}}}
  89. objectPromise(){if(!this._objectPromise){this._objectPromise=new Promise(this.requestObject.bind(this));}
  90. return this._objectPromise;}
  91. _setBackingStorage(backingStorage){if(!backingStorage){return;}
  92. this._backingStorage=backingStorage;this.args={};}}
  93. export class AsyncEvent extends Event{constructor(startEvent){super(startEvent.categoriesString,startEvent.name,startEvent.phase,startEvent.startTime,startEvent.thread);this.addArgs(startEvent.args);this.steps=[startEvent];}
  94. _addStep(event){this.steps.push(event);if(event.phase===Phase.AsyncEnd||event.phase===Phase.NestableAsyncEnd){this.setEndTime(event.startTime);this.steps[0].setEndTime(event.startTime);}}}
  95. class ProfileEventsGroup{constructor(event){this.children=[event];}
  96. _addChild(event){this.children.push(event);}}
  97. class NamedObject{constructor(model,id){this._model=model;this._id=id;this._name='';this._sortIndex=0;}
  98. static _sort(array){function comparator(a,b){return a._sortIndex!==b._sortIndex?a._sortIndex-b._sortIndex:a.name().localeCompare(b.name());}
  99. return array.sort(comparator);}
  100. _setName(name){this._name=name;}
  101. name(){return this._name;}
  102. _setSortIndex(sortIndex){this._sortIndex=sortIndex;}}
  103. export class Process extends NamedObject{constructor(model,id){super(model,id);this._threads=new Map();this._threadByName=new Map();}
  104. id(){return this._id;}
  105. threadById(id){let thread=this._threads.get(id);if(!thread){thread=new Thread(this,id);this._threads.set(id,thread);}
  106. return thread;}
  107. threadByName(name){return this._threadByName.get(name)||null;}
  108. _setThreadByName(name,thread){this._threadByName.set(name,thread);}
  109. _addEvent(payload){return this.threadById(payload.tid)._addEvent(payload);}
  110. sortedThreads(){return NamedObject._sort(this._threads.valuesArray());}}
  111. export class Thread extends NamedObject{constructor(process,id){super(process._model,id);this._process=process;this._events=[];this._asyncEvents=[];this._lastTopLevelEvent=null;}
  112. tracingComplete(){this._asyncEvents.sort(Event.compareStartTime);this._events.sort(Event.compareStartTime);const phases=Phase;const stack=[];for(let i=0;i<this._events.length;++i){const e=this._events[i];e.ordinal=i;switch(e.phase){case phases.End:this._events[i]=null;if(!stack.length){continue;}
  113. const top=stack.pop();if(top.name!==e.name||top.categoriesString!==e.categoriesString){console.error('B/E events mismatch at '+top.startTime+' ('+top.name+') vs. '+e.startTime+' ('+e.name+')');}else{top._complete(e);}
  114. break;case phases.Begin:stack.push(e);break;}}
  115. while(stack.length){stack.pop().setEndTime(this._model.maximumRecordTime());}
  116. this._events.remove(null,false);}
  117. _addEvent(payload){const event=payload.ph===Phase.SnapshotObject?ObjectSnapshot.fromPayload(payload,this):Event.fromPayload(payload,this);if(TracingModel.isTopLevelEvent(event)){if(this._lastTopLevelEvent&&this._lastTopLevelEvent.endTime>event.startTime){return null;}
  118. this._lastTopLevelEvent=event;}
  119. this._events.push(event);return event;}
  120. _addAsyncEvent(asyncEvent){this._asyncEvents.push(asyncEvent);}
  121. _setName(name){super._setName(name);this._process._setThreadByName(name,this);}
  122. id(){return this._id;}
  123. process(){return this._process;}
  124. events(){return this._events;}
  125. asyncEvents(){return this._asyncEvents;}}
  126. self.SDK=self.SDK||{};SDK=SDK||{};SDK.TracingModel=TracingModel;SDK.TracingModel.Phase=Phase;SDK.TracingModel.MetadataEvent=MetadataEvent;SDK.TracingModel.LegacyTopLevelEventCategory=LegacyTopLevelEventCategory;SDK.TracingModel.DevToolsMetadataEventCategory=DevToolsMetadataEventCategory;SDK.TracingModel.DevToolsTimelineEventCategory=DevToolsTimelineEventCategory;SDK.TracingModel.Event=Event;SDK.TracingModel.ObjectSnapshot=ObjectSnapshot;SDK.TracingModel.AsyncEvent=AsyncEvent;SDK.TracingModel.Process=Process;SDK.TracingModel.Thread=Thread;SDK.BackingStorage=BackingStorage;