blob: 5b431f6d1b906b5ab8e8e2f5b3f7c7967d92346b [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// See also timeline_message_handler.js.
var pendingRequests;
var loadingOverlay;
function onModelLoaded(model) {
viewer.globalMode = true;
viewer.model = model;
}
function clearTimeline() {
viewer.model = undefined;
}
function onImportFail(err) {
var overlay = new tr.ui.b.Overlay();
overlay.textContent = tr.b.normalizeException(err).message;
overlay.title = 'Import error';
overlay.visible = true;
console.log('import failed');
}
function compareTimestamp(a, b) { return a.ts - b.ts; }
function compareBeginTimestamp(a, b) { return a.begin.ts - b.begin.ts; }
function compareEndTimestamp(a, b) { return a.end.ts - b.end.ts; }
var basicModelEventsWaterfall = [
// Sort events and remove orphan async ends.
function filterUnwantedEvents(events) {
events = events.slice();
events.sort(compareTimestamp);
var threads = {};
return events.filter(function (event) {
if (event.ph === 'E') {
return threads[event.tid] && threads[event.tid].pop();
}
var result = event.args && event.args.mode === 'basic';
if (event.ph === 'B') {
threads[event.tid] = threads[event.tid] || [];
threads[event.tid].push(result);
}
return result;
});
}
];
var frameModelEventsWaterfall = [
// Sort events and remove orphan async ends.
function filterUnwantedEvents(events) {
events = events.slice();
events.sort(compareTimestamp);
var threads = {};
return events.filter(function (event) {
if (event.ph === 'E') {
if (!threads[event.tid]) {
return false
}
threads[event.tid] -= 1;
} else if (event.ph === 'B') {
threads[event.tid] = (threads[event.tid] || 0) + 1;
}
return true;
});
},
// Clone the events (we want to preserve them for dumps).
function cloneDeep(input) {
if (typeof input === 'object') {
if (Array.isArray(input)) {
return input.map(cloneDeep);
} else {
var clone = {};
Object.keys(input).forEach(function (key) {
clone[key] = cloneDeep(input[key]);
});
return clone;
}
}
return input;
},
// Group nested sync begin end sequences on every thread.
//
// Example:
// Input = [B,B,E,B,E,E,B,E,B,B,E,E]
// Output = [[B,B,E,B,E,E],[B,E],[B,B,E,E]]
function groupIsolatedPerThreadSequences(events) {
var sequences = [],
timeless = [],
threadOpen = {};
threadSequences = {};
events.forEach(function (event) {
if (event.ph === 'M') {
timeless.push(event);
} else if (event.ph === 'B') {
threadOpen[event.tid] = Math.max(threadOpen[event.tid] || 0) + 1;
threadSequences[event.tid] = threadSequences[event.tid] || [];
threadSequences[event.tid].push(event);
} else if (event.ph === 'E') {
threadSequences[event.tid].push(event);
threadOpen[event.tid] -= 1;
if (threadOpen[event.tid] == 0) {
threadSequences[event.tid].sort()
sequences.push(threadSequences[event.tid]);
threadSequences[event.tid] = [];
}
} else if (threadSequences[event.tid]){
threadSequences[event.tid] = threadSequences[event.tid] || [];
threadSequences[event.tid].push(event);
}
})
return {
timeless: timeless,
sequences: sequences
};
},
// Transform every sequence into an object for rapid begin end analysis and
// block types lookup.
//
// Example:
// Input = [B1,B2,E2,B3,E3,E1]
// Output = {
// begin: B1,
// end: E1,
// events: [B1,B2,E2,B3,E3,E1],
// isGPU: ...,
// isVSync: ...,
// isFramework: ...,
// isShiftable: ...,
// }
function sequenceToBlockDescriptor(input) {
return {
timeless: input.timeless,
blocks: input.sequences.map(function (events) {
var begin,
end,
isGPU,
isVSync,
isFramework;
events.forEach(function (event) {
if (event.ph === 'B') {
begin = begin || event;
} else if (event.ph === 'E') {
end = event;
}
});
isGPU = begin.name === 'GPU Workload';
isVSync = begin.name === 'VSYNC';
isFramework = begin.name === 'Framework Workload';
return {
begin: begin,
end: end,
events: events,
isGPU: isGPU,
isVSync: isVSync,
isFramework: isFramework,
isShiftable: !(isGPU || isVSync || isFramework)
};
})
};
},
// Remove all the blocks that ended before the first VSYNC.
// These events do not give any information to the analysis.
function removePreVSyncBlocks(input) {
input.blocks.sort(compareEndTimestamp);
var sawVSyncBlock = false;
return {
timeless: input.timeless,
blocks: input.blocks.filter(function (block) {
sawVSyncBlock = sawVSyncBlock || block.isVSync;
return sawVSyncBlock;
})
};
},
// Remove all the GPU blocks that started before the first Framework block.
// They are orphans of other frames.
function removePreFrameworkGPUBlocks(input) {
input.blocks.sort(compareBeginTimestamp);
var firstFrameworkBlockBeginTimestamp = 0;
return {
timeless: input.timeless,
blocks: input.blocks.filter(function (block) {
if (block.isFramework) {
firstFrameworkBlockBeginTimestamp =
firstFrameworkBlockBeginTimestamp || block.begin.ts;
} else if (block.isGPU) {
if (!firstFrameworkBlockBeginTimestamp) {
return false;
} else if (block.begin.ts < firstFrameworkBlockBeginTimestamp) {
return false;
}
}
return true;
})
};
},
// Merge all shiftable blocks that are between two Framework blocks.
// By merging them we preserve their relative timing.
function mergeShiftableBlocks(input) {
input.blocks.sort(compareEndTimestamp);
var begin,
end,
events = [],
shiftableBlocks = [],
blocks;
blocks = input.blocks.filter(function (block) {
if (block.isShiftable) {
begin = begin || block.begin;
end = block.end;
events = events.concat(block.events);
return false;
} else if (block.isFramework) {
if (events.length) {
shiftableBlocks.push({
begin: begin,
end: end,
events: events
});
}
}
return true;
});
if (events.length) {
shiftableBlocks.push({
begin: begin,
end: end,
events: events
});
}
return {
timeless: input.timeless,
blocks: blocks.concat(shiftableBlocks)
};
},
// Remove all VSyncs that didn't started an actual frame.
function filterFramelessVSyncs(input) {
input.blocks.sort(compareBeginTimestamp);
var lastVSyncBlock,
blocks,
vSyncBlocks = [];
blocks = input.blocks.filter(function (block) {
if (block.isVSync) {
lastVSyncBlock = block;
return false;
} else if (block.isFramework) {
vSyncBlocks.push(lastVSyncBlock);
}
return true;
});
return {
timeless: input.timeless,
blocks: blocks.concat(vSyncBlocks)
};
},
// Group blocks by type.
//
// Example:
// Input = [S1, V1, F1, V2, G1, F2, V3, G2, F3]
// Output = {
// gpu: [G1, G2],
// vsync: [V1, V2, V3],
// framework: [F1, F2, F3],
// shiftable: [S1]
// }
function groupBlocksByFrames(input) {
return {
timeless: input.timeless,
gpu: input.blocks.filter(function (b) { return b.isGPU; }),
vsync: input.blocks.filter(function (b) { return b.isVSync; }),
framework: input.blocks.filter(function (b) { return b.isFramework; }),
shiftable: input.blocks.filter(function (b) { return b.isShiftable; })
};
},
// Remove possible out of sync GPU Blocks.
// If the buffer has already delete the VSync and the Framework, but not the
// GPU it can potentially be still alive.
function groupBlocksByFrames(input) {
var gpu = input.gpu,
framework = input.framework;
while (gpu.length &&
gpu[0].begin.args.frame !== framework[0].begin.args.frame) {
gpu.shift();
}
return input;
},
// Group blocks related to the same frame.
// Input = {
// gpu: [G1, G2],
// vsync: [V1, V2],
// framework: [F1, F2],
// shiftable: [S1]
// }
// Output = [{V1, F1, G1, S1}, {V2, F2, G2}, {V3, F3, G3}]
function groupBlocksByFrames(input) {
var shiftable = input.shiftable.slice();
return {
timeless: input.timeless,
frames: input.vsync.map(function (vsync, i) {
var frame = {
begin: vsync.begin,
vsync: vsync,
framework: input.framework[i],
deadline: parseInt(vsync.begin.args.deadline) + 1000
};
if (i < input.gpu.length) {
frame.gpu = input.gpu[i];
}
if (shiftable.length && shiftable[0].begin.ts < framework.begin.ts ) {
frame.shiftable = shiftable.shift();
}
return frame;
})
};
},
// Move Framework and GPU as back in time as possible
//
// Example:
// Before
// [GPU]
// [VSYNC]
// [SHIFTABLE] [FRAMEWORK]
// After
// |[GPU]
// [VSYNC] |
// [SHIFTABLE]|[FRAMEWORK]
function shiftEvents(input) {
input.frames.forEach(function (frame) {
var earlierTimestamp = frame.vsync.end.ts,
shift;
if (frame.shiftable) {
frame.shiftable.events.forEach(function (event) {
if (event.tid === frame.framework.begin.tid) {
earlierTimestamp = Math.max(earlierTimestamp, event.ts);
}
});
}
if (frame.gpu) {
if (frame.shiftable) {
frame.shiftable.events.forEach(function (event) {
if (event.tid === frame.gpu.begin.tid) {
earlierTimestamp = Math.max(earlierTimestamp, event.ts);
}
});
}
shift = earlierTimestamp - frame.gpu.begin.ts;
frame.gpu.events.forEach(function (event) {
event.ts += shift;
});
}
shift = earlierTimestamp - frame.framework.begin.ts;
frame.framework.events.forEach(function (event) {
event.ts += shift;
});
frame.end = frame.framework.end;
if (frame.gpu && frame.framework.end.ts < frame.gpu.end.ts) {
frame.end = frame.gpu.end;
}
});
return input;
},
// Group events in frame (precomputation for next stage).
function groupEventsInFrame(input) {
input.frames.forEach(function (frame) {
var events = frame.vsync.events;
events = events.concat(frame.framework.events);
if (frame.gpu) {
events = events.concat(frame.gpu.events);
}
if (frame.shiftable) {
events = events.concat(frame.shiftable.events);
}
events.sort(compareTimestamp);
frame.events = events;
});
return input;
},
// Move frames in order to do not overlap.
//
// Example:
// Before
// |[GPU1--------------------]
// |[GPU2----]
// [VSYNC1] | [VSYNC2] |
// [SHIFTABLE1]|[FRAMEWORK1] |[FRAMEWORK2]
// After
// |[GPU1--------------------]| |[GPU2-----]
// [VSYNC1] | |[VSYNC2] |
// [SHIFTABLE1]|[FRAMEWORK1] | |[FRAMEWORK2]
// OtherExample:
// Before
// {FRAME BUDGET1-------------------------}
// {FRAME BUDGET2-------------------------}
// |[GPU1]
// |[GPU2]
// [VSYNC1] | [VSYNC2] |
// [SHIFTABLE1]|[FRAMEWORK1] |[FRAMEWORK2]
// After
// {FRAME BUDGET1-------------------------}|{FRAME BUDGET2----------------
// |[GPU1] | |[GPU2-----]
// [VSYNC1] | |[VSYNC2] |
// [SHIFTABLE1]|[FRAMEWORK1] | |[FRAMEWORK2]
function shiftBlocks(input) {
function minThreadtimestamps(frame) {
var timestamps = {};
frame.events.forEach(function (event) {
if (event.tid != undefined) {
timestamps[event.tid] = timestamps[event.tid] || event.ts;
}
});
return timestamps;
}
function maxThreadTimestamps(frame) {
var timestamps = {};
frame.events.forEach(function (event) {
if (event.tid != undefined) {
timestamps[event.tid] = event.ts;
}
});
return timestamps;
}
input.frames.slice(1).forEach(function (current, index) {
var previous = input.frames[index],
shift = Math.max(previous.end.ts, previous.deadline) - current.begin.ts,
maxThreadTimestamp = maxThreadTimestamps(previous),
minThreadTimestamp = minThreadtimestamps(current);
Object.keys(maxThreadTimestamp).forEach(function (tid) {
if (minThreadTimestamp[tid]) {
var delta = maxThreadTimestamp[tid] - minThreadTimestamp[tid];
shift = Math.max(shift, delta);
}
});
current.events.forEach(function (event) {
event.ts += shift;
});
current.deadline += shift;
});
return input;
},
// Add auxilary events to frame (Frame Budget and Frame Length).
// Example:
// Before
// |[GPU1--------------------]| |[GPU2-----]
// [VSYNC1] | |[VSYNC2] |
// [SHIFTABLE1]|[FRAMEWORK1] | |[FRAMEWORK2]
// After
// [Budget1---------------------------]|[Budget2--------------------------
// [Length1---------------------------]|[Length2------------]
// |[GPU1--------------------]| |[GPU2-----]
// [VSYNC1] | |[VSYNC2] |
// [SHIFTABLE1]|[FRAMEWORK1] | |[FRAMEWORK2]
function addAuxilaryEvents(input) {
input.frames.forEach(function (frame) {
frame.events.unshift({
args: {name: "Frame Budgets"},
name: "thread_name",
ph: "M",
pid: frame.begin.pid,
tid: "budgets",
});
frame.events.unshift({
args: {name: "Frames"},
name: "thread_name",
ph: "M",
pid: frame.begin.pid,
tid: "frames",
});
var duration = Math.floor((frame.end.ts - frame.begin.ts) / 1000),
frameName = "Frame " + duration + "ms";
frame.events = frame.events.concat({
ph: "B",
name: "Frame Budget",
cat: "budgets",
pid: frame.begin.pid,
tid: "budgets",
ts: frame.begin.ts
}, {
ph: "E",
name: "Frame Budget",
cat: "budgets",
pid: frame.begin.pid,
tid: "budgets",
ts: frame.deadline,
cname: 'rail_response'
}, {
ph: "B",
name: frameName,
cat: "frames",
pid: frame.begin.pid,
tid: "frames",
ts: frame.begin.ts
}, {
ph: "E",
name: frameName,
cat: "frames",
pid: frame.begin.pid,
tid: "frames",
ts: frame.end.ts,
cname: frame.end.ts > frame.deadline ? 'terrible' : 'good'
});
});
return input;
},
// Restore the events array used by catapult.
function linearizeBlocks(input) {
return input.frames.reduce(function (events, frame) {
return events.concat(frame.events);
}, input.timeless);
}
];
function basicModelEventsMap(events) {
return basicModelEventsWaterfall.reduce(function (input, step) {
return step(input);
}, events);
}
function frameModelEventsMap(events) {
return frameModelEventsWaterfall.reduce(function (input, step) {
return step(input);
}, events);
}
function updateTimeline(events) {
if (window.location.hash.indexOf('mode=basic') > -1) {
events = {
'stackFrames': events['stackFrames'],
'traceEvents': basicModelEventsMap(events['traceEvents'])
};
}
if (window.location.hash.indexOf('view=frame') > -1) {
events = {
'stackFrames': events['stackFrames'],
'traceEvents': frameModelEventsMap(events['traceEvents'])
};
}
var model = new tr.Model();
var importer = new tr.importer.Import(model);
var p = importer.importTracesWithProgressDialog([events]);
p.then(onModelLoaded.bind(undefined, model), onImportFail);
}
function showLoadingOverlay(msg) {
if (!loadingOverlay) {
loadingOverlay = new tr.ui.b.Overlay();
}
loadingOverlay.textContent = msg;
loadingOverlay.title = 'Loading...';
loadingOverlay.visible = true;
}
function hideLoadingOverlay() {
if (!loadingOverlay) {
return;
}
loadingOverlay.visible = false;
loadingOverlay = undefined;
}
function populateTimeline() {
updateTimeline(traceObject);
hideLoadingOverlay();
}
function saveTimeline() {
if (pendingRequests > 0) {
var overlay = new tr.ui.b.Overlay();
overlay.textContent = 'Cannot save timeline while fetching one.';
overlay.title = 'Save error';
overlay.visible = true;
console.log('cannot save timeline while fetching one.');
return;
}
if (!traceObject ||
!traceObject.traceEvents ||
(traceObject.traceEvents.length === 0)) {
var overlay = new tr.ui.b.Overlay();
overlay.textContent = 'Cannot save an empty timeline.';
overlay.title = 'Save error';
overlay.visible = true;
console.log('Cannot save an empty timeline.');
return;
}
var blob = new Blob([JSON.stringify(traceObject)],
{type: 'application/json'});
var blobUrl = URL.createObjectURL(blob);
var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
link.href = blobUrl;
var now = new Date();
var defaultFilename = 'dart-timeline-' +
now.getFullYear() +
'-' +
(now.getMonth() + 1) +
'-' +
now.getDate() +
'.json';
var filename = window.prompt('Save as', defaultFilename);
if (filename) {
link.download = filename;
link.click();
}
}
function loadTimeline() {
if (pendingRequests > 0) {
var overlay = new tr.ui.b.Overlay();
overlay.textContent = 'Cannot load timeline while fetching one.';
overlay.title = 'Save error';
overlay.visible = true;
console.log('Cannot load timeline while fetching one.');
return;
}
var inputElement = document.createElement('input');
inputElement.type = 'file';
inputElement.multiple = false;
var changeFired = false;
inputElement.addEventListener('change', function(e) {
if (changeFired)
return;
changeFired = true;
var file = inputElement.files[0];
var reader = new FileReader();
reader.onload = function(event) {
try {
traceObject = JSON.parse(event.target.result);
updateTimeline(traceObject);
} catch (error) {
tr.ui.b.Overlay.showError('Error while loading file: ' + error);
}
};
reader.onerror = function(event) {
tr.ui.b.Overlay.showError('Error while loading file: ' + event);
};
reader.onabort = function(event) {
tr.ui.b.Overlay.showError('Error while loading file: ' + event);
}
reader.readAsText(file);
});
inputElement.click();
}
function refreshTimeline() {
updateTimeline(traceObject);
}
window.addEventListener('DOMContentLoaded', function() {
var container = document.createElement('track-view-container');
container.id = 'track_view_container';
viewer = document.createElement('tr-ui-timeline-view');
viewer.track_view_container = container;
viewer.appendChild(container);
viewer.id = 'trace-viewer';
viewer.globalMode = true;
document.body.appendChild(viewer);
timeline_loaded = true;
console.log('DOMContentLoaded');
document.getElementById('trace-viewer').highlightVSync = true;
if (traceObject != undefined) {
refreshTimeline();
traceObject = undefined;
}
});
console.log('timeline.js loaded');