| #!/bin/bash |
| # Loading... <!-- |
| # Copyright (C) 2017 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| cd $(dirname $0) |
| python -m webbrowser -t "http://localhost:8000/$(basename $0)" |
| python -m SimpleHTTPServer |
| |
| <<-EOF |
| --> |
| <body> |
| <style> |
| * { |
| box-sizing: border-box; |
| } |
| |
| .main { |
| display: flex; |
| font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; |
| font-weight: 300; |
| } |
| |
| pre { |
| font-size: 12px; |
| } |
| |
| ul { |
| margin: 0; |
| padding: 0; |
| } |
| |
| li { |
| list-style: none; |
| border-radius: 3px; |
| border: solid rgba(0, 0, 0, 0) 1px; |
| padding: 3px; |
| margin-right: 5px 0; |
| } |
| |
| li.selected { |
| border: solid rgba(0, 0, 0, 0.89) 1px; |
| } |
| |
| h1 { |
| font-weight: 200; |
| margin-bottom: 0; |
| } |
| |
| h2 { |
| font-size: smaller; |
| } |
| |
| .focus { |
| flex: 1; |
| margin: 20px; |
| } |
| |
| .context { |
| flex: 0 0 25%; |
| } |
| |
| .green { |
| color: green; |
| } |
| |
| .red { |
| color: red; |
| } |
| |
| .files { |
| position: sticky; |
| top: 15px; |
| } |
| |
| .file { |
| display: flex; |
| justify-content: flex-start; |
| flex-direction: row; |
| } |
| |
| .file *:first-child { |
| flex: 0 0 300px; |
| } |
| |
| .file *:last-child { |
| flex-grow: 1; |
| } |
| |
| .version { |
| display: flex; |
| margin-bottom: 4px; |
| } |
| |
| .version li { |
| margin-right: 20px; |
| } |
| |
| input { |
| font-size: large; |
| margin: 20px 0; |
| } |
| |
| </style> |
| <script src="//unpkg.com/mithril"></script> |
| <script src="//unpkg.com/diff"></script> |
| |
| <div id="content"></div> |
| |
| <script> |
| // Remove hash bang. |
| document.body.firstChild.remove(); |
| |
| let THIS_URL = window.location.href; |
| let gDirectoryToFormatFiles; |
| let gNamesToRecords = new Map(); |
| let gFilterText = ''; |
| let gDisplayedRecords = null; |
| let gDisplayedName = null; |
| let gADevice = null; |
| let gBDevice = null; |
| let gDevices = [] |
| let gCache = new Map(); |
| |
| function isdir(url) { |
| return url[url.length - 1] == '/'; |
| } |
| |
| function isfile(url) { |
| return !isdir(url); |
| } |
| |
| function getdir(url) { |
| return url.slice(0, url.lastIndexOf('/')+1); |
| } |
| |
| let getdirectories = url => listdir(url).then(xs => xs.filter(isdir)); |
| let getfiles = url => listdir(url).then(xs => xs.filter(isfile)); |
| |
| function fetch(url) { |
| return new Promise(function(resolve, reject) { |
| let xhr = new XMLHttpRequest(); |
| xhr.open("GET", url, true); |
| xhr.onload = e => resolve({ |
| text: () => Promise.resolve(xhr.responseText), |
| }); |
| xhr.onerror = e => reject(xhr.statusText); |
| xhr.send(null); |
| }); |
| } |
| |
| function delay(t, v) { |
| return new Promise(resolve => { |
| setTimeout(resolve.bind(null, v), t) |
| }); |
| } |
| |
| // Limit the number of outstanding fetch requests to avoid causing |
| // problems in the browser. |
| let GET_URL_TOKENS = 400; |
| async function geturl(url) { |
| console.log('Fetch:', url); |
| if (gCache.has(url)) return Promise.resolve(gCache.get(url)); |
| |
| while (GET_URL_TOKENS === 0) { |
| // Retry in 1000ms +/- 250ms to avoid all the requests lining up. |
| await delay(1000 + 500 * (Math.random() - 0.5)); |
| } |
| GET_URL_TOKENS -= 1; |
| |
| return fetch(url).then(r => r.text()).then(text => { |
| gCache.set(url, text); |
| return text; |
| }).finally(() => { |
| GET_URL_TOKENS += 1; |
| }); |
| } |
| |
| function listdir(url) { |
| return geturl(url).then(text => { |
| let re = new RegExp('<li><a href="(.+)">(.+)</a>', 'g'); |
| if (window.location.href.indexOf('x20') != -1) |
| re = new RegExp('[^>]</td>\n<td>\n<a href="(.+)">(.+)</a>', 'g'); |
| let match; |
| let matches = []; |
| while (match = re.exec(text)) { |
| matches.push(match[1]); |
| } |
| return matches; |
| }); |
| } |
| |
| function getfiletext(url) { |
| if (gCache.has(url)) return gCache.get(url); |
| geturl(url).then(() => m.redraw()); |
| return ""; |
| } |
| |
| function makeFormatFileRecord(base_url, device, group_name, event_name) { |
| let url = base_url + device + 'events/' + group_name + event_name + 'format'; |
| let group = group_name.replace('/', ''); |
| let name = event_name.replace('/', ''); |
| return new FormatFileRecord(device, group, name, url); |
| } |
| |
| function findFormatFilesByDirectory() { |
| let url = getdir(THIS_URL) + 'data/'; |
| let directoryToFormatFiles = new Map(); |
| return getdirectories(url).then(directories => { |
| return Promise.all(directories.map(device => { |
| directoryToFormatFiles.set(device, []); |
| return getdirectories(url + device + 'events/').then(groups => { |
| return Promise.all(groups.map(group_name => { |
| let innerUrl = url + device + 'events/' + group_name; |
| return getdirectories(innerUrl).then(event_names => { |
| event_names.map(event_name => { |
| let record = makeFormatFileRecord( |
| url, |
| device, |
| group_name, |
| event_name); |
| directoryToFormatFiles.get(device).push(record); |
| }); |
| }); |
| })); |
| }); |
| })); |
| }).then(_ => { |
| return directoryToFormatFiles |
| }); |
| } |
| |
| class FormatFileRecord { |
| constructor(device, group, name, url) { |
| this.device = device; |
| this.group = group; |
| this.name = name; |
| this.url = url; |
| } |
| } |
| |
| function fuzzyMatch(query) { |
| let re = new RegExp(Array.from(query).join('.*')); |
| return text => text.match(re); |
| } |
| |
| function contextView(filterText, namesToRecords) { |
| let matcher = fuzzyMatch(filterText); |
| return m('.context', [ |
| m('h1', {class: 'title'}, 'Ftrace Format Explorer'), |
| m('input[type=text][placeholder=Filter]', { |
| oninput: e => gFilterText = e.target.value, |
| value: filterText, |
| }), |
| m('ul', |
| Array.from(namesToRecords.entries()) |
| .filter(e => matcher(e[0])).map(e => m('li[tabindex=0]', { |
| onfocus: () => { gDisplayedRecords = e[1]; gDisplayedName = e[0]; |
| }, |
| class: gDisplayedName == e[0] ? 'selected' : '', |
| }, e[0] + ' (' + e[1].length + ')' ))), |
| ]); |
| } |
| |
| function focusView(records) { |
| if (records == null) { |
| return m('div.focus'); |
| } |
| |
| let r1 = records.filter(r => r.device == gADevice)[0]; |
| let r2 = records.filter(r => r.device == gBDevice)[0]; |
| if (!r1) r1 = records[0]; |
| if (!r2) r2 = records[0]; |
| let f1 = getfiletext(r1.url); |
| let f2 = getfiletext(r2.url); |
| let diff = Diff.diffChars(f1, f2); |
| |
| let es = diff.map(part => { |
| let color = part.added ? 'green' : part.removed ? 'red' : 'grey'; |
| let e = m('span.' + color, part.value); |
| return e; |
| }); |
| return m('.focus', [ |
| m('ul.version', gDevices.map(device => m('li', { |
| onclick: () => gADevice = device, |
| class: device == gADevice ? 'selected' : '', |
| }, device))), |
| m('ul.version', gDevices.map(device => m('li', { |
| onclick: () => gBDevice = device, |
| class: device == gBDevice ? 'selected' : '', |
| }, device))), |
| m('.files', [ |
| m('.file', [m('h2', gADevice), m('pre', f1)]), |
| gADevice == gBDevice ? undefined : [ |
| m('.file', [m('h2', gBDevice), m('pre', f2)]), |
| m('.file', [m('h2', 'Delta'), m('pre', es)]), |
| ] |
| ]), |
| ]); |
| } |
| |
| let root = document.getElementById('content'); |
| let App = { |
| view: function() { |
| if (!gDirectoryToFormatFiles) |
| return m('.main', 'Loading...'); |
| return m('.main', [ |
| contextView(gFilterText, gNamesToRecords), |
| focusView(gDisplayedRecords), |
| ]) |
| } |
| } |
| m.mount(root, App); |
| |
| findFormatFilesByDirectory().then(data => { |
| gDirectoryToFormatFiles = data; |
| gNamesToRecords = new Map(); |
| gDevices = Array.from(gDirectoryToFormatFiles.keys()); |
| for (let records of gDirectoryToFormatFiles.values()) { |
| for (let record of records) { |
| geturl(record.url); |
| if (gNamesToRecords.get(record.name) == null) { |
| gNamesToRecords.set(record.name, []); |
| } |
| gNamesToRecords.get(record.name).push(record); |
| } |
| } |
| [gADevice, gBDevice] = gDevices; |
| m.redraw(); |
| }); |
| |
| </script> |
| |
| <!-- |
| EOF |
| #--> |