text: // Copyright 2018 the V8 project authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  'use strict';
  
  const trace_file_reader_template =
      document.currentScript.ownerDocument.querySelector(
          '#trace-file-reader-template');
  
  class TraceFileReader extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(trace_file_reader_template.content.cloneNode(true));
      this.addEventListener('click', e => this.handleClick(e));
      this.addEventListener('dragover', e => this.handleDragOver(e));
      this.addEventListener('drop', e => this.handleChange(e));
      this.$('#file').addEventListener('change', e => this.handleChange(e));
      this.$('#fileReader').addEventListener('keydown', e => this.handleKeyEvent(e));
    }
  
    $(id) {
      return this.shadowRoot.querySelector(id);
    }
  
    get section() {
      return this.$('#fileReaderSection');
    }
  
    updateLabel(text) {
      this.$('#label').innerText = text;
    }
  
    handleKeyEvent(event) {
      if (event.key == "Enter") this.handleClick(event);
    }
  
    handleClick(event) {
      this.$('#file').click();
    }
  
    handleChange(event) {
      // Used for drop and file change.
      event.preventDefault();
      var host = event.dataTransfer ? event.dataTransfer : event.target;
      this.readFile(host.files[0]);
    }
  
    handleDragOver(event) {
      event.preventDefault();
    }
  
    connectedCallback() {
      this.$('#fileReader').focus();
    }
  
    readFile(file) {
      if (!file) {
        this.updateLabel('Failed to load file.');
        return;
      }
      this.$('#fileReader').blur();
  
      this.section.className = 'loading';
      const reader = new FileReader();
  
      if (['application/gzip', 'application/x-gzip'].includes(file.type)) {
        reader.onload = (e) => {
          try {
            const textResult = pako.inflate(e.target.result, {to: 'string'});
            this.processRawText(file, textResult);
            this.section.className = 'success';
            this.$('#fileReader').classList.add('done');
          } catch (err) {
            console.error(err);
            this.section.className = 'failure';
          }
        };
        // Delay the loading a bit to allow for CSS animations to happen.
        setTimeout(() => reader.readAsArrayBuffer(file), 10);
      } else {
        reader.onload = (e) => this.processRawText(file, e.target.result);
        setTimeout(() => reader.readAsText(file), 10);
      }
    }
  
    processRawText(file, result) {
      let contents = result.split('\n');
      const return_data = (result.includes('V8.GC_Objects_Stats')) ?
          this.createModelFromChromeTraceFile(contents) :
          this.createModelFromV8TraceFile(contents);
      this.extendAndSanitizeModel(return_data);
      this.updateLabel('Finished loading \'' + file.name + '\'.');
      this.dispatchEvent(new CustomEvent(
          'change', {bubbles: true, composed: true, detail: return_data}));
    }
  
    createOrUpdateEntryIfNeeded(data, entry) {
      console.assert(entry.isolate, 'entry should have an isolate');
      if (!(entry.isolate in data)) {
        data[entry.isolate] = new Isolate(entry.isolate);
      }
      const data_object = data[entry.isolate];
      if (('id' in entry) && !(entry.id in data_object.gcs)) {
        data_object.gcs[entry.id] = {non_empty_instance_types: new Set()};
      }
      if ('time' in entry) {
        if (data_object.end === null || data_object.end < entry.time) {
          data_object.end = entry.time;
        }
        if (data_object.start === null || data_object.start > entry.time) {
          data_object.start = entry.time;
        }
      }
    }
  
    createDatasetIfNeeded(data, entry, data_set) {
      if (!(data_set in data[entry.isolate].gcs[entry.id])) {
        data[entry.isolate].gcs[entry.id][data_set] = {
          instance_type_data: {},
          non_empty_instance_types: new Set(),
          overall: 0
        };
        data[entry.isolate].data_sets.add(data_set);
      }
    }
  
    addFieldTypeData(data, isolate, gc_id, data_set, tagged_fields,
                     embedder_fields, unboxed_double_fields, other_raw_fields) {
      data[isolate].gcs[gc_id][data_set].field_data = {
        tagged_fields,
        embedder_fields,
        unboxed_double_fields,
        other_raw_fields
      };
    }
  
    addInstanceTypeData(data, isolate, gc_id, data_set, instance_type, entry) {
      data[isolate].gcs[gc_id][data_set].instance_type_data[instance_type] = {
        overall: entry.overall,
        count: entry.count,
        histogram: entry.histogram,
        over_allocated: entry.over_allocated,
        over_allocated_histogram: entry.over_allocated_histogram
      };
      data[isolate].gcs[gc_id][data_set].overall += entry.overall;
      if (entry.overall !== 0) {
        data[isolate].gcs[gc_id][data_set].non_empty_instance_types.add(
            instance_type);
        data[isolate].gcs[gc_id].non_empty_instance_types.add(instance_type);
        data[isolate].non_empty_instance_types.add(instance_type);
      }
    }
  
    extendAndSanitizeModel(data) {
      const checkNonNegativeProperty = (obj, property) => {
        console.assert(obj[property] >= 0, 'negative property', obj, property);
      };
  
      Object.values(data).forEach(isolate => isolate.finalize());
    }
  
    createModelFromChromeTraceFile(contents) {
      // Trace files support two formats.
      // {traceEvents: [ data ]}
      const kObjectTraceFile = {
        name: 'object',
        endToken: ']}',
        getDataArray: o => o.traceEvents
      };
      // [ data ]
      const kArrayTraceFile = {
        name: 'array',
        endToken: ']',
        getDataArray: o => o
      };
      const handler =
          (contents[0][0] === '{') ? kObjectTraceFile : kArrayTraceFile;
      console.log(`Processing log as chrome trace file (${handler.name}).`);
  
      // Pop last line in log as it might be broken.
      contents.pop();
      // Remove trailing comma.
      contents[contents.length - 1] = contents[contents.length - 1].slice(0, -1);
      // Terminate JSON.
      const sanitized_contents = [...contents, handler.endToken].join('');
  
      const data = Object.create(null);  // Final data container.
      try {
        const raw_data = JSON.parse(sanitized_contents);
        const raw_array_data = handler.getDataArray(raw_data);
        raw_array_data.filter(e => e.name === 'V8.GC_Objects_Stats')
            .forEach(trace_data => {
              const actual_data = trace_data.args;
              const data_sets = new Set(Object.keys(actual_data));
              Object.keys(actual_data).forEach(data_set => {
                const string_entry = actual_data[data_set];
                try {
                  const entry = JSON.parse(string_entry);
                  this.createOrUpdateEntryIfNeeded(data, entry);
                  this.createDatasetIfNeeded(data, entry, data_set);
                  const isolate = entry.isolate;
                  const time = entry.time;
                  const gc_id = entry.id;
                  data[isolate].gcs[gc_id].time = time;
  
                  const field_data = entry.field_data;
                  this.addFieldTypeData(data, isolate, gc_id, data_set,
                    field_data.tagged_fields, field_data.embedder_fields,
                    field_data.unboxed_double_fields,
                    field_data.other_raw_fields);
  
                  data[isolate].gcs[gc_id][data_set].bucket_sizes =
                      entry.bucket_sizes;
                  for (let [instance_type, value] of Object.entries(
                           entry.type_data)) {
                    // Trace file format uses markers that do not have actual
                    // properties.
                    if (!('overall' in value)) continue;
                    this.addInstanceTypeData(
                        data, isolate, gc_id, data_set, instance_type, value);
                  }
                } catch (e) {
                  console.log('Unable to parse data set entry', e);
                }
              });
            });
      } catch (e) {
        console.error('Unable to parse chrome trace file.', e);
      }
      return data;
    }
  
    createModelFromV8TraceFile(contents) {
      console.log('Processing log as V8 trace file.');
      contents = contents.map(function(line) {
        try {
          // Strip away a potentially present adb logcat prefix.
          line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, '');
          return JSON.parse(line);
        } catch (e) {
          console.log('Unable to parse line: \'' + line + '\'\' (' + e + ')');
        }
        return null;
      });
  
      const data = Object.create(null);  // Final data container.
      for (var entry of contents) {
        if (entry === null || entry.type === undefined) {
          continue;
        }
        if (entry.type === 'zone') {
          this.createOrUpdateEntryIfNeeded(data, entry);
          const stacktrace = ('stacktrace' in entry) ? entry.stacktrace : [];
          data[entry.isolate].samples.zone[entry.time] = {
            allocated: entry.allocated,
            pooled: entry.pooled,
            stacktrace: stacktrace
          };
        } else if (
            entry.type === 'zonecreation' || entry.type === 'zonedestruction') {
          this.createOrUpdateEntryIfNeeded(data, entry);
          data[entry.isolate].zonetags.push(
              Object.assign({opening: entry.type === 'zonecreation'}, entry));
        } else if (entry.type === 'gc_descriptor') {
          this.createOrUpdateEntryIfNeeded(data, entry);
          data[entry.isolate].gcs[entry.id].time = entry.time;
          if ('zone' in entry)
            data[entry.isolate].gcs[entry.id].malloced = entry.zone;
        } else if (entry.type === 'field_data') {
          this.createOrUpdateEntryIfNeeded(data, entry);
          this.createDatasetIfNeeded(data, entry, entry.key);
          this.addFieldTypeData(data, entry.isolate, entry.id, entry.key,
            entry.tagged_fields, entry.embedder_fields,
            entry.unboxed_double_fields, entry.other_raw_fields);
        } else if (entry.type === 'instance_type_data') {
          if (entry.id in data[entry.isolate].gcs) {
            this.createOrUpdateEntryIfNeeded(data, entry);
            this.createDatasetIfNeeded(data, entry, entry.key);
            this.addInstanceTypeData(
                data, entry.isolate, entry.id, entry.key,
                entry.instance_type_name, entry);
          }
        } else if (entry.type === 'bucket_sizes') {
          if (entry.id in data[entry.isolate].gcs) {
            this.createOrUpdateEntryIfNeeded(data, entry);
            this.createDatasetIfNeeded(data, entry, entry.key);
            data[entry.isolate].gcs[entry.id][entry.key].bucket_sizes =
                entry.sizes;
          }
        } else {
          console.log('Unknown entry type: ' + entry.type);
        }
      }
      return data;
    }
  }
  
  customElements.define('trace-file-reader', TraceFileReader);
  