Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1284)

Unified Diff: Source/devtools/front_end/timeline/TimelineModel.js

Issue 715803002: DevTools: merge TracingTimelineModel into TimelineModel (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: Source/devtools/front_end/timeline/TimelineModel.js
diff --git a/Source/devtools/front_end/timeline/TimelineModel.js b/Source/devtools/front_end/timeline/TimelineModel.js
index 579555069137dd262a28c3e4c90ee55ab9c3a7b9..43262572d90ce396e26d9f5c22137af0739f4745 100644
--- a/Source/devtools/front_end/timeline/TimelineModel.js
+++ b/Source/devtools/front_end/timeline/TimelineModel.js
@@ -30,16 +30,25 @@
/**
* @constructor
+ * @param {!WebInspector.TracingManager} tracingManager
+ * @param {!WebInspector.TracingModel} tracingModel
+ * @param {!WebInspector.TimelineModel.Filter} recordFilter
* @extends {WebInspector.Object}
*/
-WebInspector.TimelineModel = function()
+WebInspector.TimelineModel = function(tracingManager, tracingModel, recordFilter)
{
WebInspector.Object.call(this);
this._filters = [];
+ this._tracingManager = tracingManager;
+ this._tracingModel = tracingModel;
+ this._recordFilter = recordFilter;
+ this._tracingManager.addEventListener(WebInspector.TracingManager.Events.TracingStarted, this._onTracingStarted, this);
+ this._tracingManager.addEventListener(WebInspector.TracingManager.Events.EventsCollected, this._onEventsCollected, this);
+ this._tracingManager.addEventListener(WebInspector.TracingManager.Events.TracingComplete, this._onTracingComplete, this);
+ this.reset();
}
WebInspector.TimelineModel.RecordType = {
- Root: "Root",
Program: "Program",
EventDispatch: "EventDispatch",
@@ -47,21 +56,28 @@ WebInspector.TimelineModel.RecordType = {
RequestMainThreadFrame: "RequestMainThreadFrame",
BeginFrame: "BeginFrame",
+ BeginMainThreadFrame: "BeginMainThreadFrame",
ActivateLayerTree: "ActivateLayerTree",
DrawFrame: "DrawFrame",
ScheduleStyleRecalculation: "ScheduleStyleRecalculation",
RecalculateStyles: "RecalculateStyles",
InvalidateLayout: "InvalidateLayout",
Layout: "Layout",
+ UpdateLayer: "UpdateLayer",
UpdateLayerTree: "UpdateLayerTree",
PaintSetup: "PaintSetup",
Paint: "Paint",
+ PaintImage: "PaintImage",
Rasterize: "Rasterize",
+ RasterTask: "RasterTask",
ScrollLayer: "ScrollLayer",
- DecodeImage: "DecodeImage",
- ResizeImage: "ResizeImage",
CompositeLayers: "CompositeLayers",
+ StyleRecalcInvalidationTracking: "StyleRecalcInvalidationTracking",
+ LayoutInvalidationTracking: "LayoutInvalidationTracking",
+ LayerInvalidationTracking: "LayerInvalidationTracking",
+ PaintInvalidationTracking: "PaintInvalidationTracking",
+
ParseHTML: "ParseHTML",
TimerInstall: "TimerInstall",
@@ -86,6 +102,8 @@ WebInspector.TimelineModel.RecordType = {
FunctionCall: "FunctionCall",
GCEvent: "GCEvent",
+ JSFrame: "JSFrame",
+ JSSample: "JSSample",
UpdateCounters: "UpdateCounters",
@@ -99,6 +117,24 @@ WebInspector.TimelineModel.RecordType = {
WebSocketDestroy : "WebSocketDestroy",
EmbedderCallback : "EmbedderCallback",
+
+ CallStack: "CallStack",
+ SetLayerTreeId: "SetLayerTreeId",
+ TracingStartedInPage: "TracingStartedInPage",
+ TracingSessionIdForWorker: "TracingSessionIdForWorker",
+
+ DecodeImage: "Decode Image",
+ ResizeImage: "Resize Image",
+ DrawLazyPixelRef: "Draw LazyPixelRef",
+ DecodeLazyPixelRef: "Decode LazyPixelRef",
+
+ LazyPixelRef: "LazyPixelRef",
+ LayerTreeHostImplSnapshot: "cc::LayerTreeHostImpl",
+ PictureSnapshot: "cc::Picture",
+
+ // CpuProfile is a virtual event created on frontend to support
+ // serialization of CPU Profiles within tracing timeline data.
+ CpuProfile: "CpuProfile"
}
WebInspector.TimelineModel.Events = {
@@ -141,18 +177,258 @@ WebInspector.TimelineModel.forAllRecords = function(recordsArray, preOrderCallba
WebInspector.TimelineModel.TransferChunkLengthBytes = 5000000;
+/**
+ * @constructor
+ * @param {string} name
+ */
+WebInspector.TimelineModel.VirtualThread = function(name)
+{
+ this.name = name;
+ /** @type {!Array.<!WebInspector.TracingModel.Event>} */
+ this.events = [];
+ /** @type {!Array.<!Array.<!WebInspector.TracingModel.Event>>} */
+ this.asyncEvents = [];
+}
+
+/**
+ * @constructor
+ * @param {!WebInspector.TimelineModel} model
+ * @param {!WebInspector.TracingModel.Event} traceEvent
+ */
+WebInspector.TimelineModel.Record = function(model, traceEvent)
+{
+ this._model = model;
+ this._event = traceEvent;
+ traceEvent._timelineRecord = this;
+ this._children = [];
+}
+
+WebInspector.TimelineModel.Record.prototype = {
+ /**
+ * @return {?Array.<!ConsoleAgent.CallFrame>}
+ */
+ callSiteStackTrace: function()
+ {
+ var initiator = this._event.initiator;
+ return initiator ? initiator.stackTrace : null;
+ },
+
+ /**
+ * @return {?WebInspector.TimelineModel.Record}
+ */
+ initiator: function()
+ {
+ var initiator = this._event.initiator;
+ return initiator ? initiator._timelineRecord : null;
+ },
+
+ /**
+ * @return {?WebInspector.Target}
+ */
+ target: function()
+ {
+ return this._event.thread.target();
+ },
+
+ /**
+ * @return {number}
+ */
+ selfTime: function()
+ {
+ return this._event.selfTime;
+ },
+
+ /**
+ * @return {!Array.<!WebInspector.TimelineModel.Record>}
+ */
+ children: function()
+ {
+ return this._children;
+ },
+
+ /**
+ * @return {number}
+ */
+ startTime: function()
+ {
+ return this._event.startTime;
+ },
+
+ /**
+ * @return {string}
+ */
+ thread: function()
+ {
+ if (this._event.thread.name() === "CrRendererMain")
+ return WebInspector.TimelineModel.MainThreadName;
+ return this._event.thread.name();
+ },
+
+ /**
+ * @return {number}
+ */
+ endTime: function()
+ {
+ return this._endTime || this._event.endTime || this._event.startTime;
+ },
+
+ /**
+ * @param {number} endTime
+ */
+ setEndTime: function(endTime)
+ {
+ this._endTime = endTime;
+ },
+
+ /**
+ * @return {!Object}
+ */
+ data: function()
+ {
+ return this._event.args["data"];
+ },
+
+ /**
+ * @return {string}
+ */
+ type: function()
+ {
+ if (this._event.category === WebInspector.TracingModel.ConsoleEventCategory)
+ return WebInspector.TimelineModel.RecordType.ConsoleTime;
+ return this._event.name;
+ },
+
+ /**
+ * @return {string}
+ */
+ frameId: function()
+ {
+ switch (this._event.name) {
+ case WebInspector.TimelineModel.RecordType.ScheduleStyleRecalculation:
+ case WebInspector.TimelineModel.RecordType.RecalculateStyles:
+ case WebInspector.TimelineModel.RecordType.InvalidateLayout:
+ return this._event.args["frameId"];
+ case WebInspector.TimelineModel.RecordType.Layout:
+ return this._event.args["beginData"]["frameId"];
+ default:
+ var data = this._event.args["data"];
+ return (data && data["frame"]) || "";
+ }
+ },
+
+ /**
+ * @return {?Array.<!ConsoleAgent.CallFrame>}
+ */
+ stackTrace: function()
+ {
+ return this._event.stackTrace;
+ },
+
+ /**
+ * @param {string} key
+ * @return {?Object}
+ */
+ getUserObject: function(key)
+ {
+ if (key === "TimelineUIUtils::preview-element")
+ return this._event.previewElement;
+ throw new Error("Unexpected key: " + key);
+ },
+
+ /**
+ * @param {string} key
+ * @param {?Object|undefined} value
+ */
+ setUserObject: function(key, value)
+ {
+ if (key !== "TimelineUIUtils::preview-element")
+ throw new Error("Unexpected key: " + key);
+ this._event.previewElement = /** @type {?Element} */ (value);
+ },
+
+ /**
+ * @return {?Array.<string>}
+ */
+ warnings: function()
+ {
+ if (this._event.warning)
+ return [this._event.warning];
+ return null;
+ },
+
+ /**
+ * @return {!WebInspector.TracingModel.Event}
+ */
+ traceEvent: function()
+ {
+ return this._event;
+ },
+
+ /**
+ * @param {!WebInspector.TimelineModel.Record} child
+ */
+ _addChild: function(child)
+ {
+ this._children.push(child);
+ child.parent = this;
+ },
+
+ /**
+ * @return {!WebInspector.TimelineModel}
+ */
+ timelineModel: function()
+ {
+ return this._model;
+ }
+}
+
WebInspector.TimelineModel.prototype = {
/**
* @param {boolean} captureCauses
+ * @param {boolean} enableJSSampling
* @param {boolean} captureMemory
* @param {boolean} capturePictures
*/
- startRecording: function(captureCauses, captureMemory, capturePictures)
+ startRecording: function(captureCauses, enableJSSampling, captureMemory, capturePictures)
{
+ function disabledByDefault(category)
+ {
+ return "disabled-by-default-" + category;
+ }
+ var categoriesArray = [
+ "-*",
+ disabledByDefault("devtools.timeline"),
+ disabledByDefault("devtools.timeline.frame"),
+ WebInspector.TracingModel.ConsoleEventCategory
+ ];
+ if (captureCauses || enableJSSampling)
+ categoriesArray.push(disabledByDefault("devtools.timeline.stack"));
+ if (enableJSSampling) {
+ this._jsProfilerStarted = true;
+ this._currentTarget = WebInspector.context.flavor(WebInspector.Target);
+ this._configureCpuProfilerSamplingInterval();
+ this._currentTarget.profilerAgent().start();
+ }
+ if (captureCauses && Runtime.experiments.isEnabled("timelineInvalidationTracking"))
+ categoriesArray.push(disabledByDefault("devtools.timeline.invalidationTracking"));
+ if (capturePictures) {
+ categoriesArray = categoriesArray.concat([
+ disabledByDefault("devtools.timeline.layers"),
+ disabledByDefault("devtools.timeline.picture"),
+ disabledByDefault("blink.graphics_context_annotations")]);
+ }
+ var categories = categoriesArray.join(",");
+ this._startRecordingWithCategories(categories);
},
stopRecording: function()
{
+ if (this._jsProfilerStarted) {
+ this._stopCallbackBarrier = new CallbackBarrier();
+ this._currentTarget.profilerAgent().stop(this._stopCallbackBarrier.createCallback(this._didStopRecordingJSSamples.bind(this)));
+ this._jsProfilerStarted = false;
+ }
+ this._tracingManager.stop();
},
/**
@@ -230,218 +506,640 @@ WebInspector.TimelineModel.prototype = {
},
/**
- * @param {!Blob} file
- * @param {!WebInspector.Progress} progress
+ * @param {!Array.<!WebInspector.TracingManager.EventPayload>} events
*/
- loadFromFile: function(file, progress)
+ setEventsForTest: function(events)
{
- var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress);
- var fileReader = this._createFileReader(file, delegate);
- var loader = this.createLoader(fileReader, progress);
- fileReader.start(loader);
+ this._startCollectingTraceEvents(false);
+ this._tracingModel.addEvents(events);
+ this._onTracingComplete();
},
- /**
- * @param {!WebInspector.ChunkedFileReader} fileReader
- * @param {!WebInspector.Progress} progress
- * @return {!WebInspector.OutputStream}
- */
- createLoader: function(fileReader, progress)
+ _configureCpuProfilerSamplingInterval: function()
{
- throw new Error("Not implemented.");
+ var intervalUs = WebInspector.settings.highResolutionCpuProfiling.get() ? 100 : 1000;
+ this._currentTarget.profilerAgent().setSamplingInterval(intervalUs, didChangeInterval);
+
+ function didChangeInterval(error)
+ {
+ if (error)
+ WebInspector.console.error(error);
+ }
},
- _createFileReader: function(file, delegate)
+ /**
+ * @param {string} categories
+ */
+ _startRecordingWithCategories: function(categories)
{
- return new WebInspector.ChunkedFileReader(file, WebInspector.TimelineModel.TransferChunkLengthBytes, delegate);
+ this._tracingManager.start(categories, "");
},
- _createFileWriter: function()
+ _onTracingStarted: function()
{
- return new WebInspector.FileOutputStream();
+ this._startCollectingTraceEvents(false);
},
- saveToFile: function()
+ /**
+ * @param {boolean} fromFile
+ */
+ _startCollectingTraceEvents: function(fromFile)
{
- var now = new Date();
- var fileName = "TimelineRawData-" + now.toISO8601Compact() + ".json";
- var stream = this._createFileWriter();
-
- /**
- * @param {boolean} accepted
- * @this {WebInspector.TimelineModel}
- */
- function callback(accepted)
- {
- if (!accepted)
- return;
- this.writeToStream(stream);
- }
- stream.open(fileName, callback.bind(this));
+ this.reset();
+ this._tracingModel.reset();
+ this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStarted, { fromFile: fromFile });
},
/**
- * @param {!WebInspector.OutputStream} stream
+ * @param {!WebInspector.Event} event
*/
- writeToStream: function(stream)
+ _onEventsCollected: function(event)
{
- throw new Error("Not implemented.");
+ var traceEvents = /** @type {!Array.<!WebInspector.TracingManager.EventPayload>} */ (event.data);
+ this._tracingModel.addEvents(traceEvents);
},
- reset: function()
+ _onTracingComplete: function()
{
- this._records = [];
- this._minimumRecordTime = 0;
- this._maximumRecordTime = 0;
- /** @type {!Array.<!WebInspector.TimelineModel.Record>} */
- this._mainThreadTasks = [];
- /** @type {!Array.<!WebInspector.TimelineModel.Record>} */
- this._gpuThreadTasks = [];
- /** @type {!Array.<!WebInspector.TimelineModel.Record>} */
- this._eventDividerRecords = [];
- this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordsCleared);
+ if (this._stopCallbackBarrier)
+ this._stopCallbackBarrier.callWhenDone(this._didStopRecordingTraceEvents.bind(this));
+ else
+ this._didStopRecordingTraceEvents();
},
/**
- * @return {number}
+ * @param {?Protocol.Error} error
+ * @param {?ProfilerAgent.CPUProfile} cpuProfile
*/
- minimumRecordTime: function()
+ _didStopRecordingJSSamples: function(error, cpuProfile)
{
- throw new Error("Not implemented.");
+ if (error)
+ WebInspector.console.error(error);
+ this._recordedCpuProfile = cpuProfile;
},
- /**
- * @return {number}
- */
- maximumRecordTime: function()
+ _didStopRecordingTraceEvents: function()
{
- throw new Error("Not implemented.");
+ this._stopCallbackBarrier = null;
+
+ if (this._recordedCpuProfile) {
+ this._injectCpuProfileEvent(this._recordedCpuProfile);
+ this._recordedCpuProfile = null;
+ }
+ this._tracingModel.tracingComplete();
+
+ var events = this._tracingModel.devtoolsPageMetadataEvents();
+ var workerMetadataEvents = this._tracingModel.devtoolsWorkerMetadataEvents();
+
+ this._resetProcessingState();
+ for (var i = 0, length = events.length; i < length; i++) {
+ var event = events[i];
+ var process = event.thread.process();
+ var startTime = event.startTime;
+
+ var endTime = Infinity;
+ if (i + 1 < length)
+ endTime = events[i + 1].startTime;
+
+ var threads = process.sortedThreads();
+ for (var j = 0; j < threads.length; j++) {
+ var thread = threads[j];
+ if (thread.name() === "WebCore: Worker" && !workerMetadataEvents.some(function(e) { return e.args["data"]["workerThreadId"] === thread.id(); }))
+ continue;
+ this._processThreadEvents(startTime, endTime, event.thread, thread);
+ }
+ }
+ this._resetProcessingState();
+
+ this._inspectedTargetEvents.sort(WebInspector.TracingModel.Event.compareStartTime);
+
+ if (this._cpuProfile) {
+ this._processCpuProfile(this._cpuProfile);
+ this._cpuProfile = null;
+ }
+ this._buildTimelineRecords();
+ this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStopped);
},
/**
- * @return {boolean}
+ * @param {!ProfilerAgent.CPUProfile} cpuProfile
*/
- isEmpty: function()
+ _injectCpuProfileEvent: function(cpuProfile)
{
- return this.minimumRecordTime() === 0 && this.maximumRecordTime() === 0;
+ var metaEvent = this._tracingModel.devtoolsPageMetadataEvents().peekLast();
+ if (!metaEvent)
+ return;
+ var cpuProfileEvent = /** @type {!WebInspector.TracingManager.EventPayload} */ ({
+ cat: WebInspector.TracingModel.DevToolsMetadataEventCategory,
+ ph: WebInspector.TracingModel.Phase.Instant,
+ ts: this._tracingModel.maximumRecordTime() * 1000,
+ pid: metaEvent.thread.process().id(),
+ tid: metaEvent.thread.id(),
+ name: WebInspector.TimelineModel.RecordType.CpuProfile,
+ args: { data: { cpuProfile: cpuProfile } }
+ });
+ this._tracingModel.addEvents([cpuProfileEvent]);
},
/**
- * @return {!Array.<!WebInspector.TimelineModel.Record>}
+ * @param {!ProfilerAgent.CPUProfile} cpuProfile
*/
- mainThreadTasks: function()
+ _processCpuProfile: function(cpuProfile)
{
- return this._mainThreadTasks;
+ var jsSamples = WebInspector.TimelineJSProfileProcessor.generateTracingEventsFromCpuProfile(this, cpuProfile);
+ this._inspectedTargetEvents = this._inspectedTargetEvents.mergeOrdered(jsSamples, WebInspector.TracingModel.Event.orderedCompareStartTime);
+ this._setMainThreadEvents(this.mainThreadEvents().mergeOrdered(jsSamples, WebInspector.TracingModel.Event.orderedCompareStartTime));
+ var jsFrameEvents = WebInspector.TimelineJSProfileProcessor.generateJSFrameEvents(this.mainThreadEvents());
+ this._setMainThreadEvents(jsFrameEvents.mergeOrdered(this.mainThreadEvents(), WebInspector.TracingModel.Event.orderedCompareStartTime));
+ this._inspectedTargetEvents = jsFrameEvents.mergeOrdered(this._inspectedTargetEvents, WebInspector.TracingModel.Event.orderedCompareStartTime);
},
- /**
- * @return {!Array.<!WebInspector.TimelineModel.Record>}
- */
- gpuThreadTasks: function()
+ _buildTimelineRecords: function()
{
- return this._gpuThreadTasks;
+ var topLevelRecords = this._buildTimelineRecordsForThread(this.mainThreadEvents());
+
+ /**
+ * @param {!WebInspector.TimelineModel.Record} a
+ * @param {!WebInspector.TimelineModel.Record} b
+ * @return {number}
+ */
+ function compareRecordStartTime(a, b)
+ {
+ // Never return 0 as otherwise equal records would be merged.
+ return (a.startTime() <= b.startTime()) ? -1 : +1;
+ }
+
+ /**
+ * @param {!WebInspector.TimelineModel.VirtualThread} virtualThread
+ * @this {!WebInspector.TimelineModel}
+ */
+ function processVirtualThreadEvents(virtualThread)
+ {
+ var threadRecords = this._buildTimelineRecordsForThread(virtualThread.events);
+ topLevelRecords = topLevelRecords.mergeOrdered(threadRecords, compareRecordStartTime);
+ }
+ this.virtualThreads().forEach(processVirtualThreadEvents.bind(this));
+
+
+ for (var i = 0; i < topLevelRecords.length; i++) {
+ var record = topLevelRecords[i];
+ if (record.type() === WebInspector.TimelineModel.RecordType.Program)
+ this._mainThreadTasks.push(record);
+ if (record.type() === WebInspector.TimelineModel.RecordType.GPUTask)
+ this._gpuThreadTasks.push(record);
+ }
+ this._records = topLevelRecords;
},
/**
+ * @param {!Array.<!WebInspector.TracingModel.Event>} threadEvents
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
- eventDividerRecords: function()
+ _buildTimelineRecordsForThread: function(threadEvents)
{
- return this._eventDividerRecords;
- },
+ var recordStack = [];
+ var topLevelRecords = [];
+
+ for (var i = 0, size = threadEvents.length; i < size; ++i) {
+ var event = threadEvents[i];
+ for (var top = recordStack.peekLast(); top && top._event.endTime <= event.startTime; top = recordStack.peekLast()) {
+ recordStack.pop();
+ if (!recordStack.length)
+ topLevelRecords.push(top);
+ }
+ if (event.phase === WebInspector.TracingModel.Phase.AsyncEnd || event.phase === WebInspector.TracingModel.Phase.NestableAsyncEnd)
+ continue;
+ var parentRecord = recordStack.peekLast();
+ // Maintain the back-end logic of old timeline, skip console.time() / console.timeEnd() that are not properly nested.
+ if (WebInspector.TracingModel.isAsyncBeginPhase(event.phase) && parentRecord && event.endTime > parentRecord._event.endTime)
+ continue;
+ var record = new WebInspector.TimelineModel.Record(this, event);
+ if (WebInspector.TimelineUIUtils.isMarkerEvent(event))
+ this._eventDividerRecords.push(record);
+ if (!this._recordFilter.accept(record))
+ continue;
+ if (parentRecord)
+ parentRecord._addChild(record);
+ if (event.endTime)
+ recordStack.push(record);
+ }
- __proto__: WebInspector.Object.prototype
-}
+ if (recordStack.length)
+ topLevelRecords.push(recordStack[0]);
-/**
- * @interface
- */
-WebInspector.TimelineModel.Record = function()
-{
-}
+ return topLevelRecords;
+ },
-WebInspector.TimelineModel.Record.prototype = {
- /**
- * @return {?Array.<!ConsoleAgent.CallFrame>}
- */
- callSiteStackTrace: function() { },
+ _resetProcessingState: function()
+ {
+ this._sendRequestEvents = {};
+ this._timerEvents = {};
+ this._requestAnimationFrameEvents = {};
+ this._invalidationTracker = new WebInspector.InvalidationTracker();
+ this._layoutInvalidate = {};
+ this._lastScheduleStyleRecalculation = {};
+ this._webSocketCreateEvents = {};
+ this._paintImageEventByPixelRefId = {};
+ this._lastPaintForLayer = {};
+ this._lastRecalculateStylesEvent = null;
+ this._currentScriptEvent = null;
+ this._eventStack = [];
+ },
/**
- * @return {?WebInspector.TimelineModel.Record}
+ * @param {number} startTime
+ * @param {?number} endTime
+ * @param {!WebInspector.TracingModel.Thread} mainThread
+ * @param {!WebInspector.TracingModel.Thread} thread
*/
- initiator: function() { },
+ _processThreadEvents: function(startTime, endTime, mainThread, thread)
+ {
+ var events = thread.events();
+ var length = events.length;
+ var i = events.lowerBound(startTime, function (time, event) { return time - event.startTime });
+
+ var threadEvents;
+ if (thread === mainThread) {
+ threadEvents = this._mainThreadEvents;
+ this._mainThreadAsyncEvents = this._mainThreadAsyncEvents.concat(thread.asyncEvents());
+ } else {
+ var virtualThread = new WebInspector.TimelineModel.VirtualThread(thread.name());
+ threadEvents = virtualThread.events;
+ virtualThread.asyncEvents = virtualThread.asyncEvents.concat(thread.asyncEvents());
+ this._virtualThreads.push(virtualThread);
+ }
+
+ this._eventStack = [];
+ for (; i < length; i++) {
+ var event = events[i];
+ if (endTime && event.startTime >= endTime)
+ break;
+ this._processEvent(event);
+ threadEvents.push(event);
+ this._inspectedTargetEvents.push(event);
+ }
+ },
/**
- * @return {?WebInspector.Target}
+ * @param {!WebInspector.TracingModel.Event} event
*/
- target: function() { },
+ _processEvent: function(event)
+ {
+ var recordTypes = WebInspector.TimelineModel.RecordType;
+
+ var eventStack = this._eventStack;
+ while (eventStack.length && eventStack.peekLast().endTime < event.startTime)
+ eventStack.pop();
+ var duration = event.duration;
+ if (duration) {
+ if (eventStack.length) {
+ var parent = eventStack.peekLast();
+ parent.selfTime -= duration;
+ }
+ event.selfTime = duration;
+ eventStack.push(event);
+ }
+
+ if (this._currentScriptEvent && event.startTime > this._currentScriptEvent.endTime)
+ this._currentScriptEvent = null;
+
+ switch (event.name) {
+ case recordTypes.CallStack:
+ var lastMainThreadEvent = this.mainThreadEvents().peekLast();
+ if (lastMainThreadEvent && event.args["stack"] && event.args["stack"].length)
+ lastMainThreadEvent.stackTrace = event.args["stack"];
+ break;
+
+ case recordTypes.CpuProfile:
+ this._cpuProfile = event.args["data"]["cpuProfile"];
+ break;
+
+ case recordTypes.ResourceSendRequest:
+ this._sendRequestEvents[event.args["data"]["requestId"]] = event;
+ event.imageURL = event.args["data"]["url"];
+ break;
+
+ case recordTypes.ResourceReceiveResponse:
+ case recordTypes.ResourceReceivedData:
+ case recordTypes.ResourceFinish:
+ event.initiator = this._sendRequestEvents[event.args["data"]["requestId"]];
+ if (event.initiator)
+ event.imageURL = event.initiator.imageURL;
+ break;
+
+ case recordTypes.TimerInstall:
+ this._timerEvents[event.args["data"]["timerId"]] = event;
+ break;
+
+ case recordTypes.TimerFire:
+ event.initiator = this._timerEvents[event.args["data"]["timerId"]];
+ break;
+
+ case recordTypes.RequestAnimationFrame:
+ this._requestAnimationFrameEvents[event.args["data"]["id"]] = event;
+ break;
+
+ case recordTypes.FireAnimationFrame:
+ event.initiator = this._requestAnimationFrameEvents[event.args["data"]["id"]];
+ break;
+
+ case recordTypes.ScheduleStyleRecalculation:
+ this._lastScheduleStyleRecalculation[event.args["frame"]] = event;
+ break;
+
+ case recordTypes.RecalculateStyles:
+ this._invalidationTracker.didRecalcStyle(event);
+ event.initiator = this._lastScheduleStyleRecalculation[event.args["frame"]];
+ this._lastRecalculateStylesEvent = event;
+ break;
+
+ case recordTypes.StyleRecalcInvalidationTracking:
+ case recordTypes.LayoutInvalidationTracking:
+ case recordTypes.LayerInvalidationTracking:
+ case recordTypes.PaintInvalidationTracking:
+ this._invalidationTracker.addInvalidation(event);
+ break;
+
+ case recordTypes.InvalidateLayout:
+ // Consider style recalculation as a reason for layout invalidation,
+ // but only if we had no earlier layout invalidation records.
+ var layoutInitator = event;
+ var frameId = event.args["frame"];
+ if (!this._layoutInvalidate[frameId] && this._lastRecalculateStylesEvent && this._lastRecalculateStylesEvent.endTime > event.startTime)
+ layoutInitator = this._lastRecalculateStylesEvent.initiator;
+ this._layoutInvalidate[frameId] = layoutInitator;
+ break;
+
+ case recordTypes.Layout:
+ this._invalidationTracker.didLayout(event);
+ var frameId = event.args["beginData"]["frame"];
+ event.initiator = this._layoutInvalidate[frameId];
+ // In case we have no closing Layout event, endData is not available.
+ if (event.args["endData"]) {
+ event.backendNodeId = event.args["endData"]["rootNode"];
+ event.highlightQuad = event.args["endData"]["root"];
+ }
+ this._layoutInvalidate[frameId] = null;
+ if (this._currentScriptEvent)
+ event.warning = WebInspector.UIString("Forced synchronous layout is a possible performance bottleneck.");
+ break;
+
+ case recordTypes.WebSocketCreate:
+ this._webSocketCreateEvents[event.args["data"]["identifier"]] = event;
+ break;
+
+ case recordTypes.WebSocketSendHandshakeRequest:
+ case recordTypes.WebSocketReceiveHandshakeResponse:
+ case recordTypes.WebSocketDestroy:
+ event.initiator = this._webSocketCreateEvents[event.args["data"]["identifier"]];
+ break;
+
+ case recordTypes.EvaluateScript:
+ case recordTypes.FunctionCall:
+ if (!this._currentScriptEvent)
+ this._currentScriptEvent = event;
+ break;
+
+ case recordTypes.SetLayerTreeId:
+ this._inspectedTargetLayerTreeId = event.args["layerTreeId"];
+ break;
+
+ case recordTypes.Paint:
+ this._invalidationTracker.didPaint(event);
+ event.highlightQuad = event.args["data"]["clip"];
+ event.backendNodeId = event.args["data"]["nodeId"];
+ var layerUpdateEvent = this._findAncestorEvent(recordTypes.UpdateLayer);
+ if (!layerUpdateEvent || layerUpdateEvent.args["layerTreeId"] !== this._inspectedTargetLayerTreeId)
+ break;
+ // Only keep layer paint events, skip paints for subframes that get painted to the same layer as parent.
+ if (!event.args["data"]["layerId"])
+ break;
+ this._lastPaintForLayer[layerUpdateEvent.args["layerId"]] = event;
+ break;
+
+ case recordTypes.PictureSnapshot:
+ var layerUpdateEvent = this._findAncestorEvent(recordTypes.UpdateLayer);
+ if (!layerUpdateEvent || layerUpdateEvent.args["layerTreeId"] !== this._inspectedTargetLayerTreeId)
+ break;
+ var paintEvent = this._lastPaintForLayer[layerUpdateEvent.args["layerId"]];
+ if (paintEvent)
+ paintEvent.picture = event;
+ break;
+
+ case recordTypes.ScrollLayer:
+ event.backendNodeId = event.args["data"]["nodeId"];
+ break;
+
+ case recordTypes.PaintImage:
+ event.backendNodeId = event.args["data"]["nodeId"];
+ event.imageURL = event.args["data"]["url"];
+ break;
+
+ case recordTypes.DecodeImage:
+ case recordTypes.ResizeImage:
+ var paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage);
+ if (!paintImageEvent) {
+ var decodeLazyPixelRefEvent = this._findAncestorEvent(recordTypes.DecodeLazyPixelRef);
+ paintImageEvent = decodeLazyPixelRefEvent && this._paintImageEventByPixelRefId[decodeLazyPixelRefEvent.args["LazyPixelRef"]];
+ }
+ if (!paintImageEvent)
+ break;
+ event.backendNodeId = paintImageEvent.backendNodeId;
+ event.imageURL = paintImageEvent.imageURL;
+ break;
+
+ case recordTypes.DrawLazyPixelRef:
+ var paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage);
+ if (!paintImageEvent)
+ break;
+ this._paintImageEventByPixelRefId[event.args["LazyPixelRef"]] = paintImageEvent;
+ event.backendNodeId = paintImageEvent.backendNodeId;
+ event.imageURL = paintImageEvent.imageURL;
+ break;
+ }
+ },
/**
- * @return {number}
+ * @param {string} name
+ * @return {?WebInspector.TracingModel.Event}
*/
- selfTime: function() { },
+ _findAncestorEvent: function(name)
+ {
+ for (var i = this._eventStack.length - 1; i >= 0; --i) {
+ var event = this._eventStack[i];
+ if (event.name === name)
+ return event;
+ }
+ return null;
+ },
/**
- * @return {!Array.<!WebInspector.TimelineModel.Record>}
+ * @param {!Blob} file
+ * @param {!WebInspector.Progress} progress
+ */
+ loadFromFile: function(file, progress)
+ {
+ var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress);
+ var fileReader = this._createFileReader(file, delegate);
+ var loader = this.createLoader(fileReader, progress);
+ fileReader.start(loader);
+ },
+
+ _createFileReader: function(file, delegate)
+ {
+ return new WebInspector.ChunkedFileReader(file, WebInspector.TimelineModel.TransferChunkLengthBytes, delegate);
+ },
+
+ _createFileWriter: function()
+ {
+ return new WebInspector.FileOutputStream();
+ },
+
+ saveToFile: function()
+ {
+ var now = new Date();
+ var fileName = "TimelineRawData-" + now.toISO8601Compact() + ".json";
+ var stream = this._createFileWriter();
+
+ /**
+ * @param {boolean} accepted
+ * @this {WebInspector.TimelineModel}
+ */
+ function callback(accepted)
+ {
+ if (!accepted)
+ return;
+ this.writeToStream(stream);
+ }
+ stream.open(fileName, callback.bind(this));
+ },
+
+ reset: function()
+ {
+ this._virtualThreads = [];
+ this._mainThreadEvents = [];
+ this._mainThreadAsyncEvents = [];
+ this._inspectedTargetEvents = [];
+
+ this._records = [];
+ /** @type {!Array.<!WebInspector.TimelineModel.Record>} */
+ this._mainThreadTasks = [];
+ /** @type {!Array.<!WebInspector.TimelineModel.Record>} */
+ this._gpuThreadTasks = [];
+ /** @type {!Array.<!WebInspector.TimelineModel.Record>} */
+ this._eventDividerRecords = [];
+ this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordsCleared);
+ },
+
+ /**
+ * @return {number}
*/
- children: function() { },
+ minimumRecordTime: function()
+ {
+ return this._tracingModel.minimumRecordTime();
+ },
/**
* @return {number}
*/
- startTime: function() { },
+ maximumRecordTime: function()
+ {
+ return this._tracingModel.maximumRecordTime();
+ },
/**
- * @return {string}
+ * @return {!Array.<!WebInspector.TracingModel.Event>}
*/
- thread: function() { },
+ inspectedTargetEvents: function()
+ {
+ return this._inspectedTargetEvents;
+ },
/**
- * @return {number}
+ * @return {!Array.<!WebInspector.TracingModel.Event>}
*/
- endTime: function() { },
+ mainThreadEvents: function()
+ {
+ return this._mainThreadEvents;
+ },
/**
- * @param {number} endTime
+ * @param {!Array.<!WebInspector.TracingModel.Event>} events
*/
- setEndTime: function(endTime) { },
+ _setMainThreadEvents: function(events)
+ {
+ this._mainThreadEvents = events;
+ },
/**
- * @return {!Object}
+ * @return {!Array.<!Array.<!WebInspector.TracingModel.Event>>}
*/
- data: function() { },
+ mainThreadAsyncEvents: function()
+ {
+ return this._mainThreadAsyncEvents;
+ },
/**
- * @return {string}
+ * @return {!Array.<!WebInspector.TimelineModel.VirtualThread>}
*/
- type: function() { },
+ virtualThreads: function()
+ {
+ return this._virtualThreads;
+ },
/**
- * @return {string}
+ * @param {!WebInspector.ChunkedFileReader} fileReader
+ * @param {!WebInspector.Progress} progress
+ * @return {!WebInspector.OutputStream}
*/
- frameId: function() { },
+ createLoader: function(fileReader, progress)
+ {
+ return new WebInspector.TracingModelLoader(this, fileReader, progress);
+ },
/**
- * @return {?Array.<!ConsoleAgent.CallFrame>}
+ * @param {!WebInspector.OutputStream} stream
*/
- stackTrace: function() { },
+ writeToStream: function(stream)
+ {
+ var saver = new WebInspector.TracingTimelineSaver(stream);
+ this._tracingModel.writeToStream(stream, saver);
+ },
/**
- * @param {string} key
- * @return {?Object}
+ * @return {boolean}
*/
- getUserObject: function(key) { },
+ isEmpty: function()
+ {
+ return this.minimumRecordTime() === 0 && this.maximumRecordTime() === 0;
+ },
/**
- * @param {string} key
- * @param {?Object|undefined} value
+ * @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
- setUserObject: function(key, value) { },
+ mainThreadTasks: function()
+ {
+ return this._mainThreadTasks;
+ },
/**
- * @return {?Array.<string>}
+ * @return {!Array.<!WebInspector.TimelineModel.Record>}
+ */
+ gpuThreadTasks: function()
+ {
+ return this._gpuThreadTasks;
+ },
+
+ /**
+ * @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
- warnings: function() { }
+ eventDividerRecords: function()
+ {
+ return this._eventDividerRecords;
+ },
+
+
+ __proto__: WebInspector.Object.prototype
}
/**
@@ -617,3 +1315,365 @@ WebInspector.TimelineModelLoadFromFileDelegate.prototype = {
}
}
}
+
+
+/**
+ * @interface
+ */
+WebInspector.TraceEventFilter = function() { }
+
+WebInspector.TraceEventFilter.prototype = {
+ /**
+ * @param {!WebInspector.TracingModel.Event} event
+ * @return {boolean}
+ */
+ accept: function(event) { }
+}
+
+/**
+ * @constructor
+ * @implements {WebInspector.TraceEventFilter}
+ * @param {!Array.<string>} eventNames
+ */
+WebInspector.TraceEventNameFilter = function(eventNames)
+{
+ this._eventNames = eventNames.keySet();
+}
+
+WebInspector.TraceEventNameFilter.prototype = {
+ /**
+ * @param {!WebInspector.TracingModel.Event} event
+ * @return {boolean}
+ */
+ accept: function(event)
+ {
+ throw new Error("Not implemented.");
+ }
+}
+
+/**
+ * @constructor
+ * @extends {WebInspector.TraceEventNameFilter}
+ * @param {!Array.<string>} includeNames
+ */
+WebInspector.InclusiveTraceEventNameFilter = function(includeNames)
+{
+ WebInspector.TraceEventNameFilter.call(this, includeNames)
+}
+
+WebInspector.InclusiveTraceEventNameFilter.prototype = {
+ /**
+ * @override
+ * @param {!WebInspector.TracingModel.Event} event
+ * @return {boolean}
+ */
+ accept: function(event)
+ {
+ return event.category === WebInspector.TracingModel.ConsoleEventCategory || !!this._eventNames[event.name];
+ },
+ __proto__: WebInspector.TraceEventNameFilter.prototype
+}
+
+/**
+ * @constructor
+ * @extends {WebInspector.TraceEventNameFilter}
+ * @param {!Array.<string>} excludeNames
+ */
+WebInspector.ExclusiveTraceEventNameFilter = function(excludeNames)
+{
+ WebInspector.TraceEventNameFilter.call(this, excludeNames)
+}
+
+WebInspector.ExclusiveTraceEventNameFilter.prototype = {
+ /**
+ * @override
+ * @param {!WebInspector.TracingModel.Event} event
+ * @return {boolean}
+ */
+ accept: function(event)
+ {
+ return !this._eventNames[event.name];
+ },
+ __proto__: WebInspector.TraceEventNameFilter.prototype
+}
+
+/**
+ * @constructor
+ * @implements {WebInspector.OutputStream}
+ * @param {!WebInspector.TimelineModel} model
+ * @param {!{cancel: function()}} reader
+ * @param {!WebInspector.Progress} progress
+ */
+WebInspector.TracingModelLoader = function(model, reader, progress)
+{
+ this._model = model;
+ this._reader = reader;
+ this._progress = progress;
+ this._buffer = "";
+ this._firstChunk = true;
+ this._loader = new WebInspector.TracingModel.Loader(model._tracingModel);
+}
+
+WebInspector.TracingModelLoader.prototype = {
+ /**
+ * @param {string} chunk
+ */
+ write: function(chunk)
+ {
+ var data = this._buffer + chunk;
+ var lastIndex = 0;
+ var index;
+ do {
+ index = lastIndex;
+ lastIndex = WebInspector.TextUtils.findBalancedCurlyBrackets(data, index);
+ } while (lastIndex !== -1)
+
+ var json = data.slice(0, index) + "]";
+ this._buffer = data.slice(index);
+
+ if (!index)
+ return;
+
+ if (this._firstChunk) {
+ this._model._startCollectingTraceEvents(true);
+ } else {
+ var commaIndex = json.indexOf(",");
+ if (commaIndex !== -1)
+ json = json.slice(commaIndex + 1);
+ json = "[" + json;
+ }
+
+ var items;
+ try {
+ items = /** @type {!Array.<!WebInspector.TracingManager.EventPayload>} */ (JSON.parse(json));
+ } catch (e) {
+ this._reportErrorAndCancelLoading("Malformed timeline data: " + e);
+ return;
+ }
+
+ if (this._firstChunk) {
+ this._firstChunk = false;
+ if (this._looksLikeAppVersion(items[0])) {
+ this._reportErrorAndCancelLoading("Old Timeline format is not supported.");
+ return;
+ }
+ }
+
+ try {
+ this._loader.loadNextChunk(items);
+ } catch(e) {
+ this._reportErrorAndCancelLoading("Malformed timeline data: " + e);
+ return;
+ }
+ },
+
+ _reportErrorAndCancelLoading: function(messsage)
+ {
+ WebInspector.console.error(messsage);
+ this._model._onTracingComplete();
+ this._model.reset();
+ this._reader.cancel();
+ this._progress.done();
+ },
+
+ _looksLikeAppVersion: function(item)
+ {
+ return typeof item === "string" && item.indexOf("Chrome") !== -1;
+ },
+
+ close: function()
+ {
+ this._loader.finish();
+ this._model._onTracingComplete();
+ }
+}
+
+/**
+ * @constructor
+ * @param {!WebInspector.OutputStream} stream
+ * @implements {WebInspector.OutputStreamDelegate}
+ */
+WebInspector.TracingTimelineSaver = function(stream)
+{
+ this._stream = stream;
+}
+
+WebInspector.TracingTimelineSaver.prototype = {
+ onTransferStarted: function()
+ {
+ this._stream.write("[");
+ },
+
+ onTransferFinished: function()
+ {
+ this._stream.write("]");
+ },
+
+ /**
+ * @param {!WebInspector.ChunkedReader} reader
+ */
+ onChunkTransferred: function(reader) { },
+
+ /**
+ * @param {!WebInspector.ChunkedReader} reader
+ * @param {!Event} event
+ */
+ onError: function(reader, event) { },
+}
+
+/**
+ * @constructor
+ * @param {!WebInspector.TracingModel.Event} event
+ */
+WebInspector.InvalidationTrackingEvent = function(event)
+{
+ this.type = event.name;
+ this.frameId = event.args["data"]["frame"];
+ this.nodeId = event.args["data"]["nodeId"];
+ this.nodeName = event.args["data"]["nodeName"];
+ this.paintId = event.args["data"]["paintId"];
+ this.reason = event.args["data"]["reason"];
+ this.stackTrace = event.args["data"]["stackTrace"];
+}
+
+/**
+ * @constructor
+ */
+WebInspector.InvalidationTracker = function()
+{
+ this._initializePerFrameState();
+}
+
+WebInspector.InvalidationTracker.prototype = {
+ /**
+ * @param {!WebInspector.TracingModel.Event} event
+ */
+ addInvalidation: function(event)
+ {
+ var invalidation = new WebInspector.InvalidationTrackingEvent(event);
+
+ this._startNewFrameIfNeeded();
+ if (!invalidation.nodeId && !invalidation.paintId) {
+ console.error("Invalidation lacks node information.");
+ console.error(invalidation);
+ }
+
+ // Record the paintIds for style recalc or layout invalidations.
+ // FIXME: This O(n^2) loop could be optimized with a map.
+ var recordTypes = WebInspector.TimelineModel.RecordType;
+ if (invalidation.type == recordTypes.PaintInvalidationTracking)
+ this._invalidationEvents.forEach(updatePaintId);
+ else
+ this._invalidationEvents.push(invalidation);
+
+ function updatePaintId(invalidationToUpdate)
+ {
+ if (invalidationToUpdate.nodeId !== invalidation.nodeId)
+ return;
+ if (invalidationToUpdate.type === recordTypes.StyleRecalcInvalidationTracking
+ || invalidationToUpdate.type === recordTypes.LayoutInvalidationTracking) {
+ invalidationToUpdate.paintId = invalidation.paintId;
+ }
+ }
+ },
+
+ /**
+ * @param {!WebInspector.TracingModel.Event} styleRecalcEvent
+ */
+ didRecalcStyle: function(styleRecalcEvent)
+ {
+ var recalcFrameId = styleRecalcEvent.args["frame"];
+ var index = this._lastStyleRecalcEventIndex;
+ var invalidationCount = this._invalidationEvents.length;
+ for (; index < invalidationCount; index++) {
+ var invalidation = this._invalidationEvents[index];
+ if (invalidation.type !== WebInspector.TimelineModel.RecordType.StyleRecalcInvalidationTracking)
+ continue;
+ if (invalidation.frameId === recalcFrameId)
+ this._addInvalidationTrackingEvent(styleRecalcEvent, invalidation);
+ }
+
+ this._lastStyleRecalcEventIndex = invalidationCount;
+ },
+
+ /**
+ * @param {!WebInspector.TracingModel.Event} layoutEvent
+ */
+ didLayout: function(layoutEvent)
+ {
+ var layoutFrameId = layoutEvent.args["beginData"]["frame"];
+ var index = this._lastLayoutEventIndex;
+ var invalidationCount = this._invalidationEvents.length;
+ for (; index < invalidationCount; index++) {
+ var invalidation = this._invalidationEvents[index];
+ if (invalidation.type !== WebInspector.TimelineModel.RecordType.LayoutInvalidationTracking)
+ continue;
+ if (invalidation.frameId === layoutFrameId)
+ this._addInvalidationTrackingEvent(layoutEvent, invalidation);
+ }
+
+ this._lastLayoutEventIndex = invalidationCount;
+ },
+
+ /**
+ * @param {!WebInspector.TracingModel.Event} paintEvent
+ */
+ didPaint: function(paintEvent)
+ {
+ this._didPaint = true;
+
+ // If a paint doesn't have a corresponding graphics layer id, it paints
+ // into its parent so add an effectivePaintId to these events.
+ var layerId = paintEvent.args["data"]["layerId"];
+ if (layerId)
+ this._lastPaintWithLayer = paintEvent;
+ if (!this._lastPaintWithLayer) {
+ console.error("Failed to find the paint container for a paint event.");
+ return;
+ }
+
+ var effectivePaintId = this._lastPaintWithLayer.args["data"]["nodeId"];
+ var frameId = paintEvent.args["data"]["frame"];
+ this._invalidationEvents.forEach(recordInvalidationForPaint.bind(this));
+
+ /**
+ * @param {!WebInspector.InvalidationTrackingEvent} invalidation
+ * @this {WebInspector.InvalidationTracker}
+ */
+ function recordInvalidationForPaint(invalidation)
+ {
+ if (invalidation.paintId === effectivePaintId && invalidation.frameId === frameId)
+ this._addInvalidationTrackingEvent(paintEvent, invalidation);
+ }
+ },
+
+ /**
+ * @param {!WebInspector.TracingModel.Event} event
+ * @param {!WebInspector.InvalidationTrackingEvent} invalidation
+ */
+ _addInvalidationTrackingEvent: function(event, invalidation)
+ {
+ if (!event.invalidationTrackingEvents)
+ event.invalidationTrackingEvents = [ invalidation ];
+ else
+ event.invalidationTrackingEvents.push(invalidation);
+ },
+
+ _startNewFrameIfNeeded: function()
+ {
+ if (!this._didPaint)
+ return;
+
+ this._initializePerFrameState();
+ },
+
+ _initializePerFrameState: function()
+ {
+ /** @type {!Array.<!WebInspector.InvalidationTrackingEvent>} */
+ this._invalidationEvents = [];
+ this._lastStyleRecalcEventIndex = 0;
+ this._lastLayoutEventIndex = 0;
+ this._lastPaintWithLayer = undefined;
+ this._didPaint = false;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698