1 #!/bin/bash 2 # Loading... <!-- 3 # Copyright (C) 2017 The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 cd $(dirname $0) 18 python -m webbrowser -t "http://localhost:8000/$(basename $0)" 19 python -m SimpleHTTPServer 20 21 <<-EOF 22 --> 23 <body> 24 <style> 25 * { 26 box-sizing: border-box; 27 } 28 29 .main { 30 display: flex; 31 font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 32 font-weight: 300; 33 } 34 35 pre { 36 font-size: 12px; 37 } 38 39 ul { 40 margin: 0; 41 padding: 0; 42 } 43 44 li { 45 list-style: none; 46 border-radius: 3px; 47 border: solid rgba(0, 0, 0, 0) 1px; 48 padding: 3px; 49 margin-right: 5px 0; 50 } 51 52 li.selected { 53 border: solid rgba(0, 0, 0, 0.89) 1px; 54 } 55 56 h1 { 57 font-weight: 200; 58 margin-bottom: 0; 59 } 60 61 h2 { 62 font-size: smaller; 63 } 64 65 .focus { 66 flex: 1; 67 margin: 20px; 68 } 69 70 .context { 71 flex: 0 0 25%; 72 } 73 74 .green { 75 color: green; 76 } 77 78 .red { 79 color: red; 80 } 81 82 .files { 83 position: sticky; 84 top: 15px; 85 } 86 87 .file { 88 display: flex; 89 justify-content: flex-start; 90 flex-direction: row; 91 } 92 93 .file *:first-child { 94 flex: 0 0 300px; 95 } 96 97 .file *:last-child { 98 flex-grow: 1; 99 } 100 101 .version { 102 display: flex; 103 margin-bottom: 4px; 104 } 105 106 .version li { 107 margin-right: 20px; 108 } 109 110 input { 111 font-size: large; 112 margin: 20px 0; 113 } 114 115 </style> 116 <script src="//unpkg.com/mithril"></script> 117 <script src="//unpkg.com/diff"></script> 118 119 <div id="content"></div> 120 121 <script> 122 // Remove hash bang. 123 document.body.firstChild.remove(); 124 125 let THIS_URL = window.location.href; 126 let gDirectoryToFormatFiles; 127 let gNamesToRecords = new Map(); 128 let gFilterText = ''; 129 let gDisplayedRecords = null; 130 let gDisplayedName = null; 131 let gADevice = null; 132 let gBDevice = null; 133 let gDevices = [] 134 let gCache = new Map(); 135 136 function isdir(url) { 137 return url[url.length - 1] == '/'; 138 } 139 140 function isfile(url) { 141 return !isdir(url); 142 } 143 144 function getdir(url) { 145 return url.slice(0, url.lastIndexOf('/')+1); 146 } 147 148 let getdirectories = url => listdir(url).then(xs => xs.filter(isdir)); 149 let getfiles = url => listdir(url).then(xs => xs.filter(isfile)); 150 151 function fetch(url) { 152 return new Promise(function(resolve, reject) { 153 let xhr = new XMLHttpRequest(); 154 xhr.open("GET", url, true); 155 xhr.onload = e => resolve({ 156 text: () => Promise.resolve(xhr.responseText), 157 }); 158 xhr.onerror = e => reject(xhr.statusText); 159 xhr.send(null); 160 }); 161 } 162 163 function geturl(url) { 164 console.log('Fetch:', url); 165 if (gCache.has(url)) return Promise.resolve(gCache.get(url)); 166 return fetch(url).then(r => r.text()).then(text => { 167 gCache.set(url, text); 168 return text; 169 }); 170 } 171 172 function listdir(url) { 173 return geturl(url).then(text => { 174 let re = new RegExp('<li><a href="(.+)">(.+)</a>', 'g'); 175 if (window.location.href.indexOf('x20') != -1) 176 re = new RegExp('[^>]</td>\n<td>\n<a href="(.+)">(.+)</a>', 'g'); 177 let match; 178 let matches = []; 179 while (match = re.exec(text)) { 180 matches.push(match[1]); 181 } 182 return matches; 183 }); 184 } 185 186 function getfiletext(url) { 187 if (gCache.has(url)) return gCache.get(url); 188 geturl(url).then(() => m.redraw()); 189 return ""; 190 } 191 192 function makeFormatFileRecord(base_url, device, group_name, event_name) { 193 let url = base_url + device + 'events/' + group_name + event_name + 'format'; 194 let group = group_name.replace('/', ''); 195 let name = event_name.replace('/', ''); 196 return new FormatFileRecord(device, group, name, url); 197 } 198 199 function findFormatFilesByDirectory() { 200 let url = getdir(THIS_URL) + 'data/'; 201 let directoryToFormatFiles = new Map(); 202 return getdirectories(url).then(directories => { 203 return Promise.all(directories.map(device => { 204 directoryToFormatFiles.set(device, []); 205 return getdirectories(url + device + 'events/').then(groups => { 206 return Promise.all(groups.map(group_name => { 207 let innerUrl = url + device + 'events/' + group_name; 208 return getdirectories(innerUrl).then(event_names => { 209 event_names.map(event_name => { 210 let record = makeFormatFileRecord( 211 url, 212 device, 213 group_name, 214 event_name); 215 directoryToFormatFiles.get(device).push(record); 216 }); 217 }); 218 })); 219 }); 220 })); 221 }).then(_ => { 222 return directoryToFormatFiles 223 }); 224 } 225 226 class FormatFileRecord { 227 constructor(device, group, name, url) { 228 this.device = device; 229 this.group = group; 230 this.name = name; 231 this.url = url; 232 } 233 } 234 235 function fuzzyMatch(query) { 236 let re = new RegExp(Array.from(query).join('.*')); 237 return text => text.match(re); 238 } 239 240 function contextView(filterText, namesToRecords) { 241 let matcher = fuzzyMatch(filterText); 242 return m('.context', [ 243 m('h1', {class: 'title'}, 'Ftrace Format Explorer'), 244 m('input[type=text][placeholder=Filter]', { 245 oninput: m.withAttr('value', value => gFilterText = value), 246 value: filterText, 247 }), 248 m('ul', 249 Array.from(namesToRecords.entries()) 250 .filter(e => matcher(e[0])).map(e => m('li[tabindex=0]', { 251 onfocus: () => { gDisplayedRecords = e[1]; gDisplayedName = e[0]; 252 }, 253 class: gDisplayedName == e[0] ? 'selected' : '', 254 }, e[0] + ' (' + e[1].length + ')' ))), 255 ]); 256 } 257 258 function focusView(records) { 259 if (records == null) { 260 return m('div.focus'); 261 } 262 263 let r1 = records.filter(r => r.device == gADevice)[0]; 264 let r2 = records.filter(r => r.device == gBDevice)[0]; 265 if (!r1) r1 = records[0]; 266 if (!r2) r2 = records[0]; 267 let f1 = getfiletext(r1.url); 268 let f2 = getfiletext(r2.url); 269 let diff = JsDiff.diffChars(f1, f2); 270 271 let es = diff.map(part => { 272 let color = part.added ? 'green' : part.removed ? 'red' : 'grey'; 273 let e = m('span.' + color, part.value); 274 return e; 275 }); 276 return m('.focus', [ 277 m('ul.version', gDevices.map(device => m('li', { 278 onclick: () => gADevice = device, 279 class: device == gADevice ? 'selected' : '', 280 }, device))), 281 m('ul.version', gDevices.map(device => m('li', { 282 onclick: () => gBDevice = device, 283 class: device == gBDevice ? 'selected' : '', 284 }, device))), 285 m('.files', [ 286 m('.file', [m('h2', gADevice), m('pre', f1)]), 287 gADevice == gBDevice ? undefined : [ 288 m('.file', [m('h2', gBDevice), m('pre', f2)]), 289 m('.file', [m('h2', 'Delta'), m('pre', es)]), 290 ] 291 ]), 292 ]); 293 } 294 295 let root = document.getElementById('content'); 296 let App = { 297 view: function() { 298 if (!gDirectoryToFormatFiles) 299 return m('.main', 'Loading...'); 300 return m('.main', [ 301 contextView(gFilterText, gNamesToRecords), 302 focusView(gDisplayedRecords), 303 ]) 304 } 305 } 306 m.mount(root, App); 307 308 findFormatFilesByDirectory().then(data => { 309 gDirectoryToFormatFiles = data; 310 gNamesToRecords = new Map(); 311 gDevices = Array.from(gDirectoryToFormatFiles.keys()); 312 for (let records of gDirectoryToFormatFiles.values()) { 313 for (let record of records) { 314 geturl(record.url); 315 if (gNamesToRecords.get(record.name) == null) { 316 gNamesToRecords.set(record.name, []); 317 } 318 gNamesToRecords.get(record.name).push(record); 319 } 320 } 321 [gADevice, gBDevice] = gDevices; 322 m.redraw(); 323 }); 324 325 </script> 326 327 <!-- 328 EOF 329 #--> 330