7
7
8
8
const fs = require ( 'fs' ) ;
9
9
const log = require ( 'lighthouse-logger' ) ;
10
+ const stream = require ( 'stream' ) ;
10
11
const stringifySafe = require ( 'json-stringify-safe' ) ;
11
12
const Metrics = require ( './traces/pwmetrics-events' ) ;
12
13
@@ -102,6 +103,68 @@ function prepareAssets(artifacts, audits) {
102
103
. then ( _ => assets ) ;
103
104
}
104
105
106
+ /**
107
+ * Generates a JSON representation of traceData line-by-line to avoid OOM due to
108
+ * very large traces.
109
+ * @param {{traceEvents: !Array} } traceData
110
+ * @return {!Iterator<string> }
111
+ */
112
+ function * traceJsonGenerator ( traceData ) {
113
+ const keys = Object . keys ( traceData ) ;
114
+
115
+ yield '{\n' ;
116
+
117
+ // Stringify and emit trace events separately to avoid a giant string in memory.
118
+ yield '"traceEvents": [\n' ;
119
+ if ( traceData . traceEvents . length > 0 ) {
120
+ const eventsIterator = traceData . traceEvents [ Symbol . iterator ] ( ) ;
121
+ // Emit first item manually to avoid a trailing comma.
122
+ const firstEvent = eventsIterator . next ( ) . value ;
123
+ yield ` ${ JSON . stringify ( firstEvent ) } ` ;
124
+ for ( const event of eventsIterator ) {
125
+ yield `,\n ${ JSON . stringify ( event ) } ` ;
126
+ }
127
+ }
128
+ yield '\n]' ;
129
+
130
+ // Emit the rest of the object (usually just `metadata`)
131
+ if ( keys . length > 1 ) {
132
+ for ( const key of keys ) {
133
+ if ( key === 'traceEvents' ) continue ;
134
+
135
+ yield `,\n"${ key } ": ${ JSON . stringify ( traceData [ key ] , null , 2 ) } ` ;
136
+ }
137
+ }
138
+
139
+ yield '}\n' ;
140
+ }
141
+
142
+ /**
143
+ * Save a trace as JSON by streaming to disk at traceFilename.
144
+ * @param {{traceEvents: !Array} } traceData
145
+ * @param {string } traceFilename
146
+ * @return {!Promise<undefined> }
147
+ */
148
+ function saveTrace ( traceData , traceFilename ) {
149
+ return new Promise ( ( resolve , reject ) => {
150
+ const traceIter = traceJsonGenerator ( traceData ) ;
151
+ // A stream that pulls in the next traceJsonGenerator chunk as writeStream
152
+ // reads from it. Closes stream with null when iteration is complete.
153
+ const traceStream = new stream . Readable ( {
154
+ read ( ) {
155
+ const next = traceIter . next ( ) ;
156
+ this . push ( next . done ? null : next . value ) ;
157
+ }
158
+ } ) ;
159
+
160
+ const writeStream = fs . createWriteStream ( traceFilename ) ;
161
+ writeStream . on ( 'finish' , resolve ) ;
162
+ writeStream . on ( 'error' , reject ) ;
163
+
164
+ traceStream . pipe ( writeStream ) ;
165
+ } ) ;
166
+ }
167
+
105
168
/**
106
169
* Writes trace(s) and associated screenshot(s) to disk.
107
170
* @param {!Artifacts } artifacts
@@ -111,28 +174,31 @@ function prepareAssets(artifacts, audits) {
111
174
*/
112
175
function saveAssets ( artifacts , audits , pathWithBasename ) {
113
176
return prepareAssets ( artifacts , audits ) . then ( assets => {
114
- assets . forEach ( ( data , index ) => {
115
- const traceFilename = `${ pathWithBasename } -${ index } .trace.json` ;
116
- fs . writeFileSync ( traceFilename , JSON . stringify ( data . traceData , null , 2 ) ) ;
117
- log . log ( 'trace file saved to disk' , traceFilename ) ;
118
-
177
+ return Promise . all ( assets . map ( ( data , index ) => {
119
178
const devtoolsLogFilename = `${ pathWithBasename } -${ index } .devtoolslog.json` ;
120
179
fs . writeFileSync ( devtoolsLogFilename , JSON . stringify ( data . devtoolsLog , null , 2 ) ) ;
121
- log . log ( 'devtools log saved to disk' , devtoolsLogFilename ) ;
180
+ log . log ( 'saveAssets' , ' devtools log saved to disk: ' + devtoolsLogFilename ) ;
122
181
123
182
const screenshotsHTMLFilename = `${ pathWithBasename } -${ index } .screenshots.html` ;
124
183
fs . writeFileSync ( screenshotsHTMLFilename , data . screenshotsHTML ) ;
125
- log . log ( 'screenshots saved to disk' , screenshotsHTMLFilename ) ;
184
+ log . log ( 'saveAssets' , ' screenshots saved to disk: ' + screenshotsHTMLFilename ) ;
126
185
127
186
const screenshotsJSONFilename = `${ pathWithBasename } -${ index } .screenshots.json` ;
128
187
fs . writeFileSync ( screenshotsJSONFilename , JSON . stringify ( data . screenshots , null , 2 ) ) ;
129
- log . log ( 'screenshots saved to disk' , screenshotsJSONFilename ) ;
130
- } ) ;
188
+ log . log ( 'saveAssets' , 'screenshots saved to disk: ' + screenshotsJSONFilename ) ;
189
+
190
+ const streamTraceFilename = `${ pathWithBasename } -${ index } .trace.json` ;
191
+ log . log ( 'saveAssets' , 'streaming trace file to disk: ' + streamTraceFilename ) ;
192
+ return saveTrace ( data . traceData , streamTraceFilename ) . then ( _ => {
193
+ log . log ( 'saveAssets' , 'trace file streamed to disk: ' + streamTraceFilename ) ;
194
+ } ) ;
195
+ } ) ) ;
131
196
} ) ;
132
197
}
133
198
134
199
module . exports = {
135
200
saveArtifacts,
136
201
saveAssets,
137
- prepareAssets
202
+ prepareAssets,
203
+ saveTrace
138
204
} ;
0 commit comments