summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/modules/io.js
blob: 08bfd034c43b9f26090ae434b262c3adc928ba95 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const { Cc, Ci } = require("chrome");

const RecordingUtils = require("devtools/shared/performance/recording-utils");
const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");

// This identifier string is used to tentatively ascertain whether or not
// a JSON loaded from disk is actually something generated by this tool.
// It isn't, of course, a definitive verification, but a Good Enough™
// approximation before continuing the import. Don't localize this.
const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;

/**
 * Helpers for importing/exporting JSON.
 */

/**
 * Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
 * @return object
 */
function getUnicodeConverter() {
  let cname = "@mozilla.org/intl/scriptableunicodeconverter";
  let converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";
  return converter;
}

/**
 * Saves a recording as JSON to a file. The provided data is assumed to be
 * acyclical, so that it can be properly serialized.
 *
 * @param object recordingData
 *        The recording data to stream as JSON.
 * @param nsILocalFile file
 *        The file to stream the data into.
 * @return object
 *         A promise that is resolved once streaming finishes, or rejected
 *         if there was an error.
 */
function saveRecordingToFile(recordingData, file) {
  recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
  recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;

  let string = JSON.stringify(recordingData);
  let inputStream = getUnicodeConverter().convertToInputStream(string);
  let outputStream = FileUtils.openSafeFileOutputStream(file);

  return new Promise(resolve => {
    NetUtil.asyncCopy(inputStream, outputStream, resolve);
  });
}

/**
 * Loads a recording stored as JSON from a file.
 *
 * @param nsILocalFile file
 *        The file to import the data from.
 * @return object
 *         A promise that is resolved once importing finishes, or rejected
 *         if there was an error.
 */
function loadRecordingFromFile(file) {
  let channel = NetUtil.newChannel({
    uri: NetUtil.newURI(file),
    loadUsingSystemPrincipal: true
  });

  channel.contentType = "text/plain";

  return new Promise((resolve, reject) => {
    NetUtil.asyncFetch(channel, (inputStream) => {
      let recordingData;

      try {
        let string = NetUtil.readInputStreamToString(inputStream,
                                                     inputStream.available());
        recordingData = JSON.parse(string);
      } catch (e) {
        reject(new Error("Could not read recording data file."));
        return;
      }

      if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
        reject(new Error("Unrecognized recording data file."));
        return;
      }

      if (!isValidSerializerVersion(recordingData.version)) {
        reject(new Error("Unsupported recording data file version."));
        return;
      }

      if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
        recordingData = convertLegacyData(recordingData);
      }

      if (recordingData.profile.meta.version === 2) {
        RecordingUtils.deflateProfile(recordingData.profile);
      }

      // If the recording has no label, set it to be the
      // filename without its extension.
      if (!recordingData.label) {
        recordingData.label = file.leafName.replace(/\.[^.]+$/, "");
      }

      resolve(recordingData);
    });
  });
}

/**
 * Returns a boolean indicating whether or not the passed in `version`
 * is supported by this serializer.
 *
 * @param number version
 * @return boolean
 */
function isValidSerializerVersion(version) {
  return !!~[
    PERF_TOOL_SERIALIZER_LEGACY_VERSION,
    PERF_TOOL_SERIALIZER_CURRENT_VERSION
  ].indexOf(version);
}

/**
 * Takes recording data (with version `1`, from the original profiler tool),
 * and massages the data to be line with the current performance tool's
 * property names and values.
 *
 * @param object legacyData
 * @return object
 */
function convertLegacyData(legacyData) {
  let { profilerData, ticksData, recordingDuration } = legacyData;

  // The `profilerData` and `ticksData` stay, but the previously unrecorded
  // fields just are empty arrays or objects.
  let data = {
    label: profilerData.profilerLabel,
    duration: recordingDuration,
    markers: [],
    frames: [],
    memory: [],
    ticks: ticksData,
    allocations: { sites: [], timestamps: [], frames: [], sizes: [] },
    profile: profilerData.profile,
    // Fake a configuration object here if there's tick data,
    // so that it can be rendered.
    configuration: {
      withTicks: !!ticksData.length,
      withMarkers: false,
      withMemory: false,
      withAllocations: false
    },
    systemHost: {},
    systemClient: {},
  };

  return data;
}

exports.saveRecordingToFile = saveRecordingToFile;
exports.loadRecordingFromFile = loadRecordingFromFile;