• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2020 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15'use strict';
16
17let tocAnchors = [];
18let lastMouseOffY = 0;
19let onloadFired = false;
20const postLoadActions = [];
21let tocEventHandlersInstalled = false;
22let resizeObserver = undefined;
23
24// Handles redirects from the old docs.perfetto.dev.
25const legacyRedirectMap = {
26  '#/contributing': '/docs/contributing/getting-started#community',
27  '#/build-instructions': '/docs/contributing/build-instructions',
28  '#/testing': '/docs/contributing/testing',
29  '#/app-instrumentation': '/docs/instrumentation/tracing-sdk',
30  '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording',
31  '#/running': '/docs/quickstart/android-tracing',
32  '#/long-traces': '/docs/concepts/config#long-traces',
33  '#/detached-mode': '/docs/concepts/detached-mode',
34  '#/heapprofd': '/docs/data-sources/native-heap-profiler',
35  '#/java-hprof': '/docs/data-sources/java-heap-profiler',
36  '#/trace-processor': '/docs/analysis/trace-processor',
37  '#/analysis': '/docs/analysis/trace-processor#annotations',
38  '#/metrics': '/docs/analysis/metrics',
39  '#/traceconv': '/docs/quickstart/traceconv',
40  '#/clock-sync': '/docs/concepts/clock-sync',
41  '#/architecture': '/docs/concepts/service-model',
42};
43
44function doAfterLoadEvent(action) {
45  if (onloadFired) {
46    return action();
47  }
48  postLoadActions.push(action);
49}
50
51function setupSandwichMenu() {
52  const header = document.querySelector('.site-header');
53  const docsNav = document.querySelector('.nav');
54  const menu = header.querySelector('.menu');
55  menu.addEventListener('click', (e) => {
56    e.preventDefault();
57
58    // If we are displaying any /docs, toggle the navbar instead (the TOC).
59    if (docsNav) {
60      // |after_first_click| is to avoid spurious transitions on page load.
61      docsNav.classList.add('after_first_click');
62      updateNav();
63      setTimeout(() => docsNav.classList.toggle('expanded'), 0);
64    } else {
65      header.classList.toggle('expanded');
66    }
67  });
68}
69
70// (Re-)Generates the Table Of Contents for docs (the right-hand-side one).
71function updateTOC() {
72  const tocContainer = document.querySelector('.docs .toc');
73  if (!tocContainer)
74    return;
75  const toc = document.createElement('ul');
76  const anchors = document.querySelectorAll('.doc a.anchor');
77  tocAnchors = [];
78  for (const anchor of anchors) {
79    const li = document.createElement('li');
80    const link = document.createElement('a');
81    link.innerText = anchor.parentElement.innerText;
82    link.href = anchor.href;
83    link.onclick = () => {
84      onScroll(link)
85    };
86    li.appendChild(link);
87    if (anchor.parentElement.tagName === 'H3')
88      li.style.paddingLeft = '10px';
89    toc.appendChild(li);
90    doAfterLoadEvent(() => {
91      tocAnchors.push(
92          {top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link});
93    });
94  }
95  tocContainer.innerHTML = '';
96  tocContainer.appendChild(toc);
97
98  // Add event handlers on the first call (can be called more than once to
99  // recompute anchors on resize).
100  if (tocEventHandlersInstalled)
101    return;
102  tocEventHandlersInstalled = true;
103  const doc = document.querySelector('.doc');
104  const passive = {passive: true};
105  if (doc) {
106    const offY = doc.offsetTop;
107    doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive);
108    doc.addEventListener('mouseleave', () => {
109      lastMouseOffY = 0;
110    }, passive);
111  }
112  window.addEventListener('scroll', () => onScroll(), passive);
113  resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => {
114                                        updateNav();
115                                        updateTOC();
116                                      }));
117  resizeObserver.observe(doc);
118}
119
120// Highlights the current TOC anchor depending on the scroll offset.
121function onMouseMove(offY, e) {
122  lastMouseOffY = e.clientY - offY;
123  onScroll();
124}
125
126function onScroll(forceHighlight) {
127  const y = document.documentElement.scrollTop + lastMouseOffY;
128  let highEl = undefined;
129  for (const x of tocAnchors) {
130    if (y < x.top)
131      continue;
132    highEl = x.obj;
133  }
134  for (const link of document.querySelectorAll('.docs .toc a')) {
135    if ((!forceHighlight && link === highEl) || (forceHighlight === link)) {
136      link.classList.add('highlighted');
137    } else {
138      link.classList.remove('highlighted');
139    }
140  }
141}
142
143// This function needs to be idempotent as it is called more than once (on every
144// resize).
145function updateNav() {
146  const curDoc = document.querySelector('.doc');
147  let curFileName = '';
148  if (curDoc)
149    curFileName = curDoc.dataset['mdFile'];
150
151  // First identify all the top-level nav entries (Quickstart, Data Sources,
152  // ...) and make them compressible.
153  const toplevelSections = document.querySelectorAll('.docs .nav > ul > li');
154  const toplevelLinks = [];
155  for (const sec of toplevelSections) {
156    const childMenu = sec.querySelector('ul');
157    if (!childMenu) {
158      // Don't make it compressible if it has no children (e.g. the very
159      // first 'Introduction' link).
160      continue;
161    }
162
163    // Don't make it compressible if the entry has an actual link (e.g. the very
164    // first 'Introduction' link), because otherwise it become ambiguous whether
165    // the link should toggle or open the link.
166    const link = sec.querySelector('a');
167    if (!link || !link.href.endsWith('#'))
168      continue;
169
170    sec.classList.add('compressible');
171
172    // Remember the compressed status as long as the page is opened, so clicking
173    // through links keeps the sidebar in a consistent visual state.
174    const memoKey = `docs.nav.compressed[${link.innerHTML}]`;
175
176    if (sessionStorage.getItem(memoKey) === '1') {
177      sec.classList.add('compressed');
178    }
179    doAfterLoadEvent(() => {
180      childMenu.style.maxHeight = `${childMenu.scrollHeight + 40}px`;
181    });
182
183    toplevelLinks.push(link);
184    link.onclick = (evt) => {
185      evt.preventDefault();
186      sec.classList.toggle('compressed');
187      if (sec.classList.contains('compressed')) {
188        sessionStorage.setItem(memoKey, '1');
189      } else {
190        sessionStorage.removeItem(memoKey);
191      }
192    };
193  }
194
195  const exps = document.querySelectorAll('.docs .nav ul a');
196  let found = false;
197  for (const x of exps) {
198    // If the url of the entry matches the url of the page, mark the item as
199    // highlighted and expand all its parents.
200    if (!x.href)
201      continue;
202    const url = new URL(x.href);
203    if (x.href.endsWith('#')) {
204      // This is a non-leaf link to a menu.
205      if (toplevelLinks.indexOf(x) < 0) {
206        x.removeAttribute('href');
207      }
208    } else if (url.pathname === curFileName && !found) {
209      x.classList.add('selected');
210      doAfterLoadEvent(() => x.scrollIntoViewIfNeeded());
211      found = true;  // Highlight only the first occurrence.
212    }
213  }
214}
215
216// If the page contains a ```mermaid ``` block, lazily loads the plugin and
217// renders.
218function initMermaid() {
219  const graphs = document.querySelectorAll('.mermaid');
220
221  // Skip if there are no mermaid graphs to render.
222  if (!graphs.length)
223    return;
224
225  const script = document.createElement('script');
226  script.type = 'text/javascript';
227  script.src = '/assets/mermaid.min.js';
228  const themeCSS = `
229  .cluster rect { fill: #FCFCFC; stroke: #ddd }
230  .node rect { fill: #DCEDC8; stroke: #8BC34A}
231  .edgeLabel:not(:empty) {
232      border-radius: 6px;
233      font-size: 0.9em;
234      padding: 4px;
235      background: #F5F5F5;
236      border: 1px solid #DDDDDD;
237      color: #666;
238  }
239  `;
240  script.addEventListener('load', () => {
241    mermaid.initialize({
242      startOnLoad: false,
243      themeCSS: themeCSS,
244      securityLevel: 'loose',  // To allow #in-page-links
245    });
246    for (const graph of graphs) {
247      requestAnimationFrame(() => {
248        mermaid.init(undefined, graph);
249        graph.classList.add('rendered');
250      });
251    }
252  })
253  document.body.appendChild(script);
254}
255
256function setupSearch() {
257  const URL =
258      'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q='
259  const searchContainer = document.getElementById('search');
260  const searchBox = document.getElementById('search-box');
261  const searchRes = document.getElementById('search-res')
262  if (!searchBox || !searchRes) return;
263
264  document.body.addEventListener('keydown', (e) => {
265    if (e.key === '/' && e.target.tagName.toLowerCase() === 'body') {
266      searchBox.setSelectionRange(0, -1);
267      searchBox.focus();
268      e.preventDefault();
269    } else if (e.key === 'Escape' && searchContainer.contains(e.target)) {
270      searchBox.blur();
271
272      // Handle the case of clicking Tab and moving down to results.
273      e.target.blur();
274    }
275  });
276
277  let timerId = -1;
278  let lastSearchId = 0;
279
280  const doSearch = async () => {
281    timerId = -1;
282    searchRes.style.width = `${searchBox.offsetWidth}px`;
283
284    // `searchId` handles the case of two subsequent requests racing. This is to
285    // prevent older results, delivered in reverse order, to replace newer ones.
286    const searchId = ++lastSearchId;
287    const f = await fetch(URL + encodeURIComponent(searchBox.value));
288    const jsonRes = await f.json();
289    const results = jsonRes['items'];
290    searchRes.innerHTML = '';
291    if (results === undefined || searchId != lastSearchId) {
292      return;
293    }
294    for (const res of results) {
295      const link = document.createElement('a');
296      link.href = res.link;
297      const title = document.createElement('div');
298      title.className = 'sr-title';
299      title.innerText = res.title.replace(' - Perfetto Tracing Docs', '');
300      link.appendChild(title);
301
302      const snippet = document.createElement('div');
303      snippet.className = 'sr-snippet';
304      snippet.innerText = res.snippet;
305      link.appendChild(snippet);
306
307      const div = document.createElement('div');
308      div.appendChild(link);
309      searchRes.appendChild(div);
310    }
311  };
312
313  searchBox.addEventListener('keyup', () => {
314    if (timerId >= 0) return;
315    timerId = setTimeout(doSearch, 200);
316  });
317}
318
319window.addEventListener('DOMContentLoaded', () => {
320  updateNav();
321  updateTOC();
322});
323
324window.addEventListener('load', () => {
325  setupSandwichMenu();
326  initMermaid();
327
328  // Don't smooth-scroll on pages that are too long (e.g. reference pages).
329  if (document.body.scrollHeight < 10000) {
330    document.documentElement.style.scrollBehavior = 'smooth';
331  } else {
332    document.documentElement.style.scrollBehavior = 'initial';
333  }
334
335  onloadFired = true;
336  while (postLoadActions.length > 0) {
337    postLoadActions.shift()();
338  }
339
340  updateTOC();
341  setupSearch();
342
343  // Enable animations only after the load event. This is to prevent glitches
344  // when switching pages.
345  document.documentElement.style.setProperty('--anim-enabled', '1')
346});
347
348const fragment = location.hash.split('?')[0].replace('.md', '');
349if (fragment in legacyRedirectMap) {
350  location.replace(legacyRedirectMap[fragment]);
351}