Commit 030ae792 authored by nednguyen@google.com's avatar nednguyen@google.com

Remove the weak dictionary that maps tab objects to it markers in tracing_timeline_data.

We add the list of tab's markers to traces as metadata and later reconstruct the maps from tab markers to renderer thread in trace_event_importer. This makes it possible to reconstruct the timeline model with the mapping between tabs & renderer threads from the raw json tracing data. 

BUG=

Review URL: https://codereview.chromium.org/309533016

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@275671 0039d316-1c4b-4281-b951-d872f2087c98
parent 0d9a2bd0
...@@ -31,7 +31,7 @@ class LoadingTrace(page_measurement.PageMeasurement): ...@@ -31,7 +31,7 @@ class LoadingTrace(page_measurement.PageMeasurement):
loading.LoadingMetric().AddResults(tab, results) loading.LoadingMetric().AddResults(tab, results)
timeline_metric = timeline.LoadTimesTimelineMetric() timeline_metric = timeline.LoadTimesTimelineMetric()
renderer_thread = \ renderer_thread = \
self._timeline_controller.model.GetRendererThreadFromTab(tab) self._timeline_controller.model.GetRendererThreadFromTabId(tab.id)
record = tir_module.TimelineInteractionRecord( record = tir_module.TimelineInteractionRecord(
"loading_trace_interaction", 0, float('inf')) "loading_trace_interaction", 0, float('inf'))
timeline_metric.AddResults( timeline_metric.AddResults(
......
...@@ -51,7 +51,8 @@ class SmoothnessController(object): ...@@ -51,7 +51,8 @@ class SmoothnessController(object):
# the time ranges of gestures, if there is at least one, else the the time # the time ranges of gestures, if there is at least one, else the the time
# ranges from the first action to the last action. # ranges from the first action to the last action.
renderer_thread = self._timeline_model.GetRendererThreadFromTab(tab) renderer_thread = self._timeline_model.GetRendererThreadFromTabId(
tab.id)
run_smooth_actions_record = None run_smooth_actions_record = None
smooth_records = [] smooth_records = []
for event in renderer_thread.async_slices: for event in renderer_thread.async_slices:
......
...@@ -39,7 +39,7 @@ class ThreadTimes(page_measurement.PageMeasurement): ...@@ -39,7 +39,7 @@ class ThreadTimes(page_measurement.PageMeasurement):
def MeasurePage(self, page, tab, results): def MeasurePage(self, page, tab, results):
metric = timeline.ThreadTimesTimelineMetric() metric = timeline.ThreadTimesTimelineMetric()
renderer_thread = \ renderer_thread = \
self._timeline_controller.model.GetRendererThreadFromTab(tab) self._timeline_controller.model.GetRendererThreadFromTabId(tab.id)
if self.options.report_silk_results: if self.options.report_silk_results:
metric.results_to_report = timeline.ReportSilkResults metric.results_to_report = timeline.ReportSilkResults
if self.options.report_silk_details: if self.options.report_silk_details:
......
...@@ -47,8 +47,8 @@ class TimelineController(object): ...@@ -47,8 +47,8 @@ class TimelineController(object):
# Stop tracing. # Stop tracing.
timeline_data = tab.browser.StopTracing() timeline_data = tab.browser.StopTracing()
self._model = TimelineModel(timeline_data) self._model = TimelineModel(timeline_data)
self._renderer_process = self._model.GetRendererProcessFromTab(tab) self._renderer_process = self._model.GetRendererProcessFromTabId(tab.id)
renderer_thread = self.model.GetRendererThreadFromTab(tab) renderer_thread = self.model.GetRendererThreadFromTabId(tab.id)
run_smooth_actions_record = None run_smooth_actions_record = None
self._smooth_records = [] self._smooth_records = []
......
...@@ -24,6 +24,7 @@ from telemetry.core.backends.chrome import extension_backend ...@@ -24,6 +24,7 @@ from telemetry.core.backends.chrome import extension_backend
from telemetry.core.backends.chrome import system_info_backend from telemetry.core.backends.chrome import system_info_backend
from telemetry.core.backends.chrome import tab_list_backend from telemetry.core.backends.chrome import tab_list_backend
from telemetry.core.backends.chrome import tracing_backend from telemetry.core.backends.chrome import tracing_backend
from telemetry.core.backends.chrome import tracing_timeline_data
from telemetry.unittest import options_for_unittests from telemetry.unittest import options_for_unittests
...@@ -282,17 +283,21 @@ class ChromeBrowserBackend(browser_backend.BrowserBackend): ...@@ -282,17 +283,21 @@ class ChromeBrowserBackend(browser_backend.BrowserBackend):
def StopTracing(self): def StopTracing(self):
""" Stops tracing and returns the result as TimelineData object. """ """ Stops tracing and returns the result as TimelineData object. """
for (i, debugger_url) in enumerate(self._browser.tabs): tab_ids_list = []
for (i, _) in enumerate(self._browser.tabs):
tab = self.tab_list_backend.Get(i, None) tab = self.tab_list_backend.Get(i, None)
if tab: if tab:
success = tab.EvaluateJavaScript( success = tab.EvaluateJavaScript(
"console.time('" + debugger_url + "');" + "console.time('" + tab.id + "');" +
"console.timeEnd('" + debugger_url + "');" + "console.timeEnd('" + tab.id + "');" +
"console.time.toString().indexOf('[native code]') != -1;") "console.time.toString().indexOf('[native code]') != -1;")
if not success: if not success:
raise Exception('Page stomped on console.time') raise Exception('Page stomped on console.time')
self._tracing_backend.AddTabToMarkerMapping(tab, debugger_url) tab_ids_list.append(tab.id)
return self._tracing_backend.StopTracing() trace_events = self._tracing_backend.StopTracing()
# Augment tab_ids data to trace events.
event_data = {'traceEvents' : trace_events, 'tabIds': tab_ids_list}
return tracing_timeline_data.TracingTimelineData(event_data)
def GetProcessName(self, cmd_line): def GetProcessName(self, cmd_line):
"""Returns a user-friendly name for the process of the given |cmd_line|.""" """Returns a user-friendly name for the process of the given |cmd_line|."""
......
...@@ -78,6 +78,10 @@ class InspectorBackend(inspector_websocket.InspectorWebsocket): ...@@ -78,6 +78,10 @@ class InspectorBackend(inspector_websocket.InspectorWebsocket):
return c['url'] return c['url']
return None return None
@property
def id(self):
return self.debugger_url
@property @property
def debugger_url(self): def debugger_url(self):
return self._context['webSocketDebuggerUrl'] return self._context['webSocketDebuggerUrl']
......
...@@ -4,10 +4,8 @@ ...@@ -4,10 +4,8 @@
import logging import logging
import re import re
import weakref
from telemetry.core.backends.chrome import inspector_websocket from telemetry.core.backends.chrome import inspector_websocket
from telemetry.core.backends.chrome import tracing_timeline_data
# All tracing categories not disabled-by-default # All tracing categories not disabled-by-default
...@@ -109,10 +107,6 @@ class TracingBackend(object): ...@@ -109,10 +107,6 @@ class TracingBackend(object):
self._category_filter = None self._category_filter = None
self._nesting = 0 self._nesting = 0
self._tracing_data = [] self._tracing_data = []
# Use a WeakKeyDictionary, because an ordinary dictionary could keep
# references to Tab objects around until it gets garbage collected.
# This would prevent telemetry from navigating to another page.
self._tab_to_marker_mapping = weakref.WeakKeyDictionary()
@property @property
def is_tracing_running(self): def is_tracing_running(self):
...@@ -164,14 +158,11 @@ class TracingBackend(object): ...@@ -164,14 +158,11 @@ class TracingBackend(object):
def _GetTraceResult(self): def _GetTraceResult(self):
assert not self.is_tracing_running assert not self.is_tracing_running
return tracing_timeline_data.TracingTimelineData( return self._tracing_data
self._tracing_data, self._tab_to_marker_mapping)
def _GetTraceResultAndReset(self): def _GetTraceResultAndReset(self):
result = self._GetTraceResult() result = self._GetTraceResult()
# Reset tab to marker mapping for the next tracing run. Don't use clear(),
# because a TraceResult may still hold a reference to the dictionary object.
self._tab_to_marker_mapping = weakref.WeakKeyDictionary()
self._tracing_data = [] self._tracing_data = []
return result return result
......
...@@ -3,36 +3,20 @@ ...@@ -3,36 +3,20 @@
# found in the LICENSE file. # found in the LICENSE file.
import json import json
import weakref
from telemetry.core.timeline_data import TimelineData from telemetry.core.timeline_data import TimelineData
class TracingTimelineData(TimelineData): class TracingTimelineData(TimelineData):
def __init__(self, event_data, tab_to_marker_mapping = None): def __init__(self, event_data):
super(TracingTimelineData, self).__init__() super(TracingTimelineData, self).__init__()
self._event_data = event_data self._event_data = event_data
if tab_to_marker_mapping == None:
self._tab_to_marker_mapping = weakref.WeakKeyDictionary()
else:
self._tab_to_marker_mapping = tab_to_marker_mapping
def Serialize(self, f): def Serialize(self, f):
"""Serializes the trace result to a file-like object""" """Serializes the trace result to a file-like object"""
f.write('{"traceEvents":') if 'traceEvents' in self._event_data:
json.dump(self._event_data, f) json.dump(self._event_data, f)
f.write('}') else:
json.dump({'traceEvents' : self._event_data}, f)
def EventData(self): def EventData(self):
return self._event_data return self._event_data
def AugmentTimelineModel(self, timeline):
for thread in timeline.GetAllThreads():
if thread.name == 'CrBrowserMain':
timeline.browser_process = thread.parent
for key, value in self._tab_to_marker_mapping.iteritems():
timeline_markers = timeline.FindTimelineMarkers(value)
assert(len(timeline_markers) == 1)
assert(timeline_markers[0].start_thread ==
timeline_markers[0].end_thread)
renderer_thread = timeline_markers[0].start_thread
timeline.AddCoreObjectToContainerMapping(key, renderer_thread)
...@@ -104,6 +104,10 @@ class WebDriverTabBackend(object): ...@@ -104,6 +104,10 @@ class WebDriverTabBackend(object):
# IE/Firefox has no timeline. # IE/Firefox has no timeline.
raise NotImplementedError() raise NotImplementedError()
@property
def id(self):
raise NotImplementedError()
def StartTimelineRecording(self): def StartTimelineRecording(self):
raise NotImplementedError() raise NotImplementedError()
......
...@@ -103,7 +103,8 @@ class TabTest(tab_test_case.TabTestCase): ...@@ -103,7 +103,8 @@ class TabTest(tab_test_case.TabTestCase):
self._tab.ClearHighlight(bitmap.WEB_PAGE_TEST_ORANGE) self._tab.ClearHighlight(bitmap.WEB_PAGE_TEST_ORANGE)
trace_data = self._browser.StopTracing() trace_data = self._browser.StopTracing()
timeline_model = model.TimelineModel(trace_data) timeline_model = model.TimelineModel(trace_data)
renderer_thread = timeline_model.GetRendererThreadFromTab(self._tab) renderer_thread = timeline_model.GetRendererThreadFromTabId(
self._tab.id)
found_video_start_event = False found_video_start_event = False
for event in renderer_thread.async_slices: for event in renderer_thread.async_slices:
if event.name == '__ClearHighlight.video_capture_start': if event.name == '__ClearHighlight.video_capture_start':
...@@ -111,6 +112,47 @@ class TabTest(tab_test_case.TabTestCase): ...@@ -111,6 +112,47 @@ class TabTest(tab_test_case.TabTestCase):
break break
self.assertTrue(found_video_start_event) self.assertTrue(found_video_start_event)
def testGetRendererThreadFromTabId(self):
self.assertEquals(self._tab.url, 'about:blank')
# Create 3 tabs. The third tab is closed before we call StartTracing.
first_tab = self._tab
second_tab = self._browser.tabs.New()
second_tab.Navigate('about:blank')
second_tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
third_tab = self._browser.tabs.New()
third_tab.Navigate('about:blank')
third_tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
third_tab.Close()
self._browser.StartTracing(tracing_backend.MINIMAL_TRACE_CATEGORIES)
first_tab.ExecuteJavaScript('console.time("first-tab-marker");')
first_tab.ExecuteJavaScript('console.timeEnd("first-tab-marker");')
second_tab.ExecuteJavaScript('console.time("second-tab-marker");')
second_tab.ExecuteJavaScript('console.timeEnd("second-tab-marker");')
trace_data = self._browser.StopTracing()
timeline_model = model.TimelineModel(trace_data)
# Assert that the renderer_thread of the first tab contains
# 'first-tab-marker'.
renderer_thread = timeline_model.GetRendererThreadFromTabId(
first_tab.id)
first_tab_markers = [
renderer_thread.IterAllSlicesOfName('first-tab-marker')]
self.assertEquals(1, len(first_tab_markers))
# Close second tab and assert that the renderer_thread of the second tab
# contains 'second-tab-marker'.
second_tab.Close()
renderer_thread = timeline_model.GetRendererThreadFromTabId(
second_tab.id)
second_tab_markers = [
renderer_thread.IterAllSlicesOfName('second-tab-marker')]
self.assertEquals(1, len(second_tab_markers))
# Third tab wasn't available when we start tracing, so there is no
# renderer_thread corresponding to it in the the trace.
self.assertIs(None, timeline_model.GetRendererThreadFromTabId(third_tab.id))
class GpuTabTest(tab_test_case.TabTestCase): class GpuTabTest(tab_test_case.TabTestCase):
def setUp(self): def setUp(self):
......
...@@ -8,11 +8,8 @@ https://code.google.com/p/trace-viewer/ ...@@ -8,11 +8,8 @@ https://code.google.com/p/trace-viewer/
''' '''
from operator import attrgetter from operator import attrgetter
import weakref
import telemetry.core.timeline.process as tracing_process import telemetry.core.timeline.process as tracing_process
from telemetry.core import web_contents
from telemetry.core import browser
# Register importers for data # Register importers for data
from telemetry.core.timeline import bounds from telemetry.core.timeline import bounds
...@@ -52,14 +49,10 @@ class TimelineModel(object): ...@@ -52,14 +49,10 @@ class TimelineModel(object):
self._processes = {} self._processes = {}
self._browser_process = None self._browser_process = None
self._frozen = False self._frozen = False
self._tab_ids_to_renderer_threads_map = {}
self.import_errors = [] self.import_errors = []
self.metadata = [] self.metadata = []
self.flow_events = [] self.flow_events = []
# Use a WeakKeyDictionary, because an ordinary dictionary could keep
# references to Tab objects around until it gets garbage collected.
# This would prevent telemetry from navigating to another page.
self._core_object_to_timeline_container_map = weakref.WeakKeyDictionary()
if timeline_data is not None: if timeline_data is not None:
self.ImportTraces(timeline_data, shift_world_to_zero=shift_world_to_zero) self.ImportTraces(timeline_data, shift_world_to_zero=shift_world_to_zero)
...@@ -85,9 +78,15 @@ class TimelineModel(object): ...@@ -85,9 +78,15 @@ class TimelineModel(object):
def browser_process(self, browser_process): def browser_process(self, browser_process):
self._browser_process = browser_process self._browser_process = browser_process
def AddMappingFromTabIdToRendererThread(self, tab_id, renderer_thread):
if self._frozen:
raise Exception('Cannot add mapping from tab id to renderer thread once '
'trace is imported')
self._tab_ids_to_renderer_threads_map[tab_id] = renderer_thread
def ImportTraces(self, timeline_data, shift_world_to_zero=True): def ImportTraces(self, timeline_data, shift_world_to_zero=True):
if self._frozen: if self._frozen:
raise Exception("Cannot add events once recording is done") raise Exception("Cannot add events once trace is imported")
importers = [] importers = []
if isinstance(timeline_data, list): if isinstance(timeline_data, list):
...@@ -232,20 +231,14 @@ class TimelineModel(object): ...@@ -232,20 +231,14 @@ class TimelineModel(object):
return events return events
def GetRendererProcessFromTab(self, tab): def GetRendererProcessFromTabId(self, tab_id):
return self._core_object_to_timeline_container_map[tab].parent renderer_thread = self.GetRendererThreadFromTabId(tab_id)
if renderer_thread:
return renderer_thread.parent
return None
def GetRendererThreadFromTab(self, tab): def GetRendererThreadFromTabId(self, tab_id):
return self._core_object_to_timeline_container_map[tab] return self._tab_ids_to_renderer_threads_map.get(tab_id, None)
def AddCoreObjectToContainerMapping(self, core_object, container):
""" Add a mapping from a core object to a timeline container.
Used for example to map a Tab to its renderer process in the timeline model.
"""
assert(isinstance(core_object, web_contents.WebContents) or
isinstance(core_object, browser.Browser))
self._core_object_to_timeline_container_map[core_object] = container
def _CreateImporter(self, event_data): def _CreateImporter(self, event_data):
for importer_class in _IMPORTERS: for importer_class in _IMPORTERS:
......
...@@ -16,6 +16,7 @@ from telemetry.core.timeline import importer ...@@ -16,6 +16,7 @@ from telemetry.core.timeline import importer
import telemetry.core.timeline.async_slice as tracing_async_slice import telemetry.core.timeline.async_slice as tracing_async_slice
import telemetry.core.timeline.flow_event as tracing_flow_event import telemetry.core.timeline.flow_event as tracing_flow_event
class TraceEventTimelineImporter(importer.TimelineImporter): class TraceEventTimelineImporter(importer.TimelineImporter):
def __init__(self, model, timeline_data): def __init__(self, model, timeline_data):
super(TraceEventTimelineImporter, self).__init__( super(TraceEventTimelineImporter, self).__init__(
...@@ -58,7 +59,7 @@ class TraceEventTimelineImporter(importer.TimelineImporter): ...@@ -58,7 +59,7 @@ class TraceEventTimelineImporter(importer.TimelineImporter):
# Any other fields in the container should be treated as metadata. # Any other fields in the container should be treated as metadata.
self._model.metadata.append({ self._model.metadata.append({
'name' : field_name, 'name' : field_name,
'value' : container['field_name']}) 'value' : container[field_name]})
@staticmethod @staticmethod
def CanImport(timeline_data): def CanImport(timeline_data):
...@@ -275,10 +276,10 @@ class TraceEventTimelineImporter(importer.TimelineImporter): ...@@ -275,10 +276,10 @@ class TraceEventTimelineImporter(importer.TimelineImporter):
self._model.UpdateBounds() self._model.UpdateBounds()
self._CreateAsyncSlices() self._CreateAsyncSlices()
self._CreateFlowSlices() self._CreateFlowSlices()
self._SetBrowserProcess()
self._CreateExplicitObjects() self._CreateExplicitObjects()
self._CreateImplicitObjects() self._CreateImplicitObjects()
self._CreateTabIdsToThreadsMap()
self._timeline_data.AugmentTimelineModel(self._model)
def _CreateAsyncSlices(self): def _CreateAsyncSlices(self):
if len(self._all_async_events) == 0: if len(self._all_async_events) == 0:
...@@ -445,3 +446,22 @@ class TraceEventTimelineImporter(importer.TimelineImporter): ...@@ -445,3 +446,22 @@ class TraceEventTimelineImporter(importer.TimelineImporter):
else: else:
# Make this event the next start event in this flow. # Make this event the next start event in this flow.
flow_id_to_event[event['id']] = flow_event flow_id_to_event[event['id']] = flow_event
def _SetBrowserProcess(self):
for thread in self._model.GetAllThreads():
if thread.name == 'CrBrowserMain':
self._model.browser_process = thread.parent
def _CreateTabIdsToThreadsMap(self):
tab_ids_list = []
for metadata in self._model.metadata:
if metadata['name'] == 'tabIds':
tab_ids_list = metadata['value']
break
for tab_id in tab_ids_list:
timeline_markers = self._model.FindTimelineMarkers(tab_id)
assert(len(timeline_markers) == 1)
assert(timeline_markers[0].start_thread ==
timeline_markers[0].end_thread)
self._model.AddMappingFromTabIdToRendererThread(
tab_id, timeline_markers[0].start_thread)
...@@ -1152,3 +1152,38 @@ class TraceEventTimelineImporterTest(unittest.TestCase): ...@@ -1152,3 +1152,38 @@ class TraceEventTimelineImporterTest(unittest.TestCase):
timeline_data = tracing_timeline_data.TracingTimelineData(events) timeline_data = tracing_timeline_data.TracingTimelineData(events)
m = timeline_model.TimelineModel(timeline_data=timeline_data) m = timeline_model.TimelineModel(timeline_data=timeline_data)
self.assertEqual(0, len(m.flow_events)) self.assertEqual(0, len(m.flow_events))
def testTraceEventsWithTabIdsMarkers(self):
trace_events = [
{'name': 'a', 'args': {}, 'pid': 1, 'ts': 20, 'tts': 10, 'cat': 'foo',
'tid': 1, 'ph': 'B'},
# tab-id-1
{'name': 'tab-id-1', 'args': {}, 'pid': 1, 'ts': 25, 'cat': 'foo',
'tid': 1,
'ph': 'S', 'id': 72},
{'name': 'a', 'args': {}, 'pid': 1, 'ts': 30, 'tts': 20, 'cat': 'foo',
'tid': 1, 'ph': 'E'},
{'name': 'tab-id-1', 'args': {}, 'pid': 1, 'ts': 35, 'cat': 'foo',
'tid': 1,
'ph': 'F', 'id': 72},
# tab-id-2
{'name': 'tab-id-2', 'args': {}, 'pid': 1, 'ts': 25, 'cat': 'foo',
'tid': 2,
'ph': 'S', 'id': 72},
{'name': 'tab-id-2', 'args': {}, 'pid': 1, 'ts': 26, 'cat': 'foo',
'tid': 2,
'ph': 'F', 'id': 72},
]
event_data = {'traceEvents': trace_events,
'tabIds': ['tab-id-1', 'tab-id-2']}
timeline_data = tracing_timeline_data.TracingTimelineData(event_data)
m = timeline_model.TimelineModel(timeline_data=timeline_data)
processes = m.GetAllProcesses()
self.assertEqual(1, len(processes))
self.assertIs(processes[0], m.GetRendererProcessFromTabId('tab-id-1'))
self.assertIs(processes[0], m.GetRendererProcessFromTabId('tab-id-2'))
p = processes[0]
self.assertEqual(2, len(p.threads))
self.assertIs(p.threads[1], m.GetRendererThreadFromTabId('tab-id-1'))
self.assertIs(p.threads[2], m.GetRendererThreadFromTabId('tab-id-2'))
...@@ -20,6 +20,11 @@ class WebContents(object): ...@@ -20,6 +20,11 @@ class WebContents(object):
'network_quiescence.js')) as f: 'network_quiescence.js')) as f:
self._quiescence_js = f.read() self._quiescence_js = f.read()
@property
def id(self):
"""Return the unique id string for this tab object."""
return self._inspector_backend.id
def WaitForDocumentReadyStateToBeComplete(self, def WaitForDocumentReadyStateToBeComplete(self,
timeout=DEFAULT_WEB_CONTENTS_TIMEOUT): timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
self.WaitForJavaScriptExpression( self.WaitForJavaScriptExpression(
......
...@@ -24,7 +24,7 @@ class ActionRunnerTest(tab_test_case.TabTestCase): ...@@ -24,7 +24,7 @@ class ActionRunnerTest(tab_test_case.TabTestCase):
timeline_model = model.TimelineModel(trace_data) timeline_model = model.TimelineModel(trace_data)
records = [] records = []
renderer_thread = timeline_model.GetRendererThreadFromTab(self._tab) renderer_thread = timeline_model.GetRendererThreadFromTabId(self._tab.id)
for event in renderer_thread.async_slices: for event in renderer_thread.async_slices:
if not tir_module.IsTimelineInteractionRecord(event.name): if not tir_module.IsTimelineInteractionRecord(event.name):
continue continue
......
...@@ -151,7 +151,7 @@ class TimelineBasedMeasurement(page_measurement.PageMeasurement): ...@@ -151,7 +151,7 @@ class TimelineBasedMeasurement(page_measurement.PageMeasurement):
logging.error('Cannot open %s. %s' % (trace_file_path, e)) logging.error('Cannot open %s. %s' % (trace_file_path, e))
model = model_module.TimelineModel(trace_result) model = model_module.TimelineModel(trace_result)
renderer_thread = model.GetRendererThreadFromTab(tab) renderer_thread = model.GetRendererThreadFromTabId(tab.id)
meta_metrics = _TimelineBasedMetrics( meta_metrics = _TimelineBasedMetrics(
model, renderer_thread, self.CreateMetricsForTimelineInteractionRecord) model, renderer_thread, self.CreateMetricsForTimelineInteractionRecord)
meta_metrics.AddResults(results) meta_metrics.AddResults(results)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment