xref: /aosp_15_r20/external/perfetto/ui/src/trace_processor/engine.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2018 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
15import protos from '../protos';
16import {defer, Deferred} from '../base/deferred';
17import {assertExists, assertTrue} from '../base/logging';
18import {ProtoRingBuffer} from './proto_ring_buffer';
19import {
20  createQueryResult,
21  QueryError,
22  QueryResult,
23  WritableQueryResult,
24} from './query_result';
25import TPM = protos.TraceProcessorRpc.TraceProcessorMethod;
26import {exists} from '../base/utils';
27import {errResult, okResult, Result} from '../base/result';
28
29export type EngineMode = 'WASM' | 'HTTP_RPC';
30export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE' | 'FORCE_BUILTIN_WASM';
31
32// This is used to skip the decoding of queryResult from protobufjs and deal
33// with it ourselves. See the comment below around `QueryResult.decode = ...`.
34interface QueryResultBypass {
35  rawQueryResult: Uint8Array;
36}
37
38export interface TraceProcessorConfig {
39  cropTrackEvents: boolean;
40  ingestFtraceInRawTable: boolean;
41  analyzeTraceProtoContent: boolean;
42  ftraceDropUntilAllCpusValid: boolean;
43}
44
45export interface Engine {
46  readonly mode: EngineMode;
47  readonly engineId: string;
48
49  /**
50   * Execute a query against the database, returning a promise that resolves
51   * when the query has completed but rejected when the query fails for whatever
52   * reason. On success, the promise will only resolve once all the resulting
53   * rows have been received.
54   *
55   * The promise will be rejected if the query fails.
56   *
57   * @param sql The query to execute.
58   * @param tag An optional tag used to trace the origin of the query.
59   */
60  query(sql: string, tag?: string): Promise<QueryResult>;
61
62  /**
63   * Execute a query against the database, returning a promise that resolves
64   * when the query has completed or failed. The promise will never get
65   * rejected, it will always successfully resolve. Use the returned wrapper
66   * object to determine whether the query completed successfully.
67   *
68   * The promise will only resolve once all the resulting rows have been
69   * received.
70   *
71   * @param sql The query to execute.
72   * @param tag An optional tag used to trace the origin of the query.
73   */
74  tryQuery(sql: string, tag?: string): Promise<Result<QueryResult>>;
75
76  /**
77   * Execute one or more metric and get the result.
78   *
79   * @param metrics The metrics to run.
80   * @param format The format of the response.
81   */
82  computeMetric(
83    metrics: string[],
84    format: 'json' | 'prototext' | 'proto',
85  ): Promise<string | Uint8Array>;
86
87  enableMetatrace(categories?: protos.MetatraceCategories): void;
88  stopAndGetMetatrace(): Promise<protos.DisableAndReadMetatraceResult>;
89
90  getProxy(tag: string): EngineProxy;
91  readonly numRequestsPending: number;
92  readonly failed: string | undefined;
93}
94
95// Abstract interface of a trace proccessor.
96// This is the TypeScript equivalent of src/trace_processor/rpc.h.
97// There are two concrete implementations:
98//   1. WasmEngineProxy: creates a Wasm module and interacts over postMessage().
99//   2. HttpRpcEngine: connects to an external `trace_processor_shell --httpd`.
100//      and interacts via fetch().
101// In both cases, we have a byte-oriented pipe to interact with TraceProcessor.
102// The derived class is only expected to deal with these two functions:
103// 1. Implement the abstract rpcSendRequestBytes() function, sending the
104//    proto-encoded TraceProcessorRpc requests to the TraceProcessor instance.
105// 2. Call onRpcResponseBytes() when response data is received.
106export abstract class EngineBase implements Engine, Disposable {
107  abstract readonly id: string;
108  abstract readonly mode: EngineMode;
109  private txSeqId = 0;
110  private rxSeqId = 0;
111  private rxBuf = new ProtoRingBuffer();
112  private pendingParses = new Array<Deferred<void>>();
113  private pendingEOFs = new Array<Deferred<void>>();
114  private pendingResetTraceProcessors = new Array<Deferred<void>>();
115  private pendingQueries = new Array<WritableQueryResult>();
116  private pendingRestoreTables = new Array<Deferred<void>>();
117  private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>();
118  private pendingReadMetatrace?: Deferred<protos.DisableAndReadMetatraceResult>;
119  private pendingRegisterSqlPackage?: Deferred<void>;
120  private _isMetatracingEnabled = false;
121  private _numRequestsPending = 0;
122  private _failed: string | undefined = undefined;
123
124  // TraceController sets this to raf.scheduleFullRedraw().
125  onResponseReceived?: () => void;
126
127  // Called to send data to the TraceProcessor instance. This turns into a
128  // postMessage() or a HTTP request, depending on the Engine implementation.
129  abstract rpcSendRequestBytes(data: Uint8Array): void;
130
131  // Called when an inbound message is received by the Engine implementation
132  // (e.g. onmessage for the Wasm case, on when HTTP replies are received for
133  // the HTTP+RPC case).
134  onRpcResponseBytes(dataWillBeRetained: Uint8Array) {
135    // Note: when hitting the fastpath inside ProtoRingBuffer, the |data| buffer
136    // is returned back by readMessage() (% subarray()-ing it) and held onto by
137    // other classes (e.g., QueryResult). For both fetch() and Wasm we are fine
138    // because every response creates a new buffer.
139    this.rxBuf.append(dataWillBeRetained);
140    for (;;) {
141      const msg = this.rxBuf.readMessage();
142      if (msg === undefined) break;
143      this.onRpcResponseMessage(msg);
144    }
145  }
146
147  // Parses a response message.
148  // |rpcMsgEncoded| is a sub-array to to the start of a TraceProcessorRpc
149  // proto-encoded message (without the proto preamble and varint size).
150  private onRpcResponseMessage(rpcMsgEncoded: Uint8Array) {
151    // Here we override the protobufjs-generated code to skip the parsing of the
152    // new streaming QueryResult and instead passing it through like a buffer.
153    // This is the overall problem: All trace processor responses are wrapped
154    // into a TraceProcessorRpc proto message. In all cases %
155    // TPM_QUERY_STREAMING, we want protobufjs to decode the proto bytes and
156    // give us a structured object. In the case of TPM_QUERY_STREAMING, instead,
157    // we want to deal with the proto parsing ourselves using the new
158    // QueryResult.appendResultBatch() method, because that handled streaming
159    // results more efficiently and skips several copies.
160    // By overriding the decode method below, we achieve two things:
161    // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field.
162    // 2. We stash (a view of) the original buffer into the |rawQueryResult| so
163    //    the `case TPM_QUERY_STREAMING` below can take it.
164    protos.QueryResult.decode = (reader: protobuf.Reader, length: number) => {
165      const res = protos.QueryResult.create() as {} as QueryResultBypass;
166      res.rawQueryResult = reader.buf.subarray(reader.pos, reader.pos + length);
167      // All this works only if protobufjs returns the original ArrayBuffer
168      // from |rpcMsgEncoded|. It should be always the case given the
169      // current implementation. This check mainly guards against future
170      // behavioral changes of protobufjs. We don't want to accidentally
171      // hold onto some internal protobufjs buffer. We are fine holding
172      // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
173      // is buffer-retention-friendly.
174      assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
175      reader.pos += length;
176      return res as {} as protos.QueryResult;
177    };
178
179    const rpc = protos.TraceProcessorRpc.decode(rpcMsgEncoded);
180
181    if (rpc.fatalError !== undefined && rpc.fatalError.length > 0) {
182      this.fail(`${rpc.fatalError}`);
183    }
184
185    // Allow restarting sequences from zero (when reloading the browser).
186    if (rpc.seq !== this.rxSeqId + 1 && this.rxSeqId !== 0 && rpc.seq !== 0) {
187      // "(ERR:rpc_seq)" is intercepted by error_dialog.ts to show a more
188      // graceful and actionable error.
189      this.fail(
190        `RPC sequence id mismatch ` +
191          `cur=${rpc.seq} last=${this.rxSeqId} (ERR:rpc_seq)`,
192      );
193    }
194
195    this.rxSeqId = rpc.seq;
196
197    let isFinalResponse = true;
198
199    switch (rpc.response) {
200      case TPM.TPM_APPEND_TRACE_DATA: {
201        const appendResult = assertExists(rpc.appendResult);
202        const pendingPromise = assertExists(this.pendingParses.shift());
203        if (exists(appendResult.error) && appendResult.error.length > 0) {
204          pendingPromise.reject(appendResult.error);
205        } else {
206          pendingPromise.resolve();
207        }
208        break;
209      }
210      case TPM.TPM_FINALIZE_TRACE_DATA: {
211        const finalizeResult = assertExists(rpc.finalizeDataResult);
212        const pendingPromise = assertExists(this.pendingEOFs.shift());
213        if (exists(finalizeResult.error) && finalizeResult.error.length > 0) {
214          pendingPromise.reject(finalizeResult.error);
215        } else {
216          pendingPromise.resolve();
217        }
218        break;
219      }
220      case TPM.TPM_RESET_TRACE_PROCESSOR:
221        assertExists(this.pendingResetTraceProcessors.shift()).resolve();
222        break;
223      case TPM.TPM_RESTORE_INITIAL_TABLES:
224        assertExists(this.pendingRestoreTables.shift()).resolve();
225        break;
226      case TPM.TPM_QUERY_STREAMING:
227        const qRes = assertExists(rpc.queryResult) as {} as QueryResultBypass;
228        const pendingQuery = assertExists(this.pendingQueries[0]);
229        pendingQuery.appendResultBatch(qRes.rawQueryResult);
230        if (pendingQuery.isComplete()) {
231          this.pendingQueries.shift();
232        } else {
233          isFinalResponse = false;
234        }
235        break;
236      case TPM.TPM_COMPUTE_METRIC:
237        const metricRes = assertExists(
238          rpc.metricResult,
239        ) as protos.ComputeMetricResult;
240        const pendingComputeMetric = assertExists(
241          this.pendingComputeMetrics.shift(),
242        );
243        if (exists(metricRes.error) && metricRes.error.length > 0) {
244          const error = new QueryError(
245            `ComputeMetric() error: ${metricRes.error}`,
246            {
247              query: 'COMPUTE_METRIC',
248            },
249          );
250          pendingComputeMetric.reject(error);
251        } else {
252          const result =
253            metricRes.metricsAsPrototext ??
254            metricRes.metricsAsJson ??
255            metricRes.metrics ??
256            '';
257          pendingComputeMetric.resolve(result);
258        }
259        break;
260      case TPM.TPM_DISABLE_AND_READ_METATRACE:
261        const metatraceRes = assertExists(
262          rpc.metatrace,
263        ) as protos.DisableAndReadMetatraceResult;
264        assertExists(this.pendingReadMetatrace).resolve(metatraceRes);
265        this.pendingReadMetatrace = undefined;
266        break;
267      case TPM.TPM_REGISTER_SQL_PACKAGE:
268        const registerResult = assertExists(rpc.registerSqlPackageResult);
269        const res = assertExists(this.pendingRegisterSqlPackage);
270        if (exists(registerResult.error) && registerResult.error.length > 0) {
271          res.reject(registerResult.error);
272        } else {
273          res.resolve();
274        }
275        break;
276      default:
277        console.log(
278          'Unexpected TraceProcessor response received: ',
279          rpc.response,
280        );
281        break;
282    } // switch(rpc.response);
283
284    if (isFinalResponse) {
285      --this._numRequestsPending;
286    }
287
288    this.onResponseReceived?.();
289  }
290
291  // TraceProcessor methods below this point.
292  // The methods below are called by the various controllers in the UI and
293  // deal with marshalling / unmarshaling requests to/from TraceProcessor.
294
295  // Push trace data into the engine. The engine is supposed to automatically
296  // figure out the type of the trace (JSON vs Protobuf).
297  parse(data: Uint8Array): Promise<void> {
298    const asyncRes = defer<void>();
299    this.pendingParses.push(asyncRes);
300    const rpc = protos.TraceProcessorRpc.create();
301    rpc.request = TPM.TPM_APPEND_TRACE_DATA;
302    rpc.appendTraceData = data;
303    this.rpcSendRequest(rpc);
304    return asyncRes; // Linearize with the worker.
305  }
306
307  // Notify the engine that we reached the end of the trace.
308  // Called after the last parse() call.
309  notifyEof(): Promise<void> {
310    const asyncRes = defer<void>();
311    this.pendingEOFs.push(asyncRes);
312    const rpc = protos.TraceProcessorRpc.create();
313    rpc.request = TPM.TPM_FINALIZE_TRACE_DATA;
314    this.rpcSendRequest(rpc);
315    return asyncRes; // Linearize with the worker.
316  }
317
318  // Updates the TraceProcessor Config. This method creates a new
319  // TraceProcessor instance, so it should be called before passing any trace
320  // data.
321  resetTraceProcessor({
322    cropTrackEvents,
323    ingestFtraceInRawTable,
324    analyzeTraceProtoContent,
325    ftraceDropUntilAllCpusValid,
326  }: TraceProcessorConfig): Promise<void> {
327    const asyncRes = defer<void>();
328    this.pendingResetTraceProcessors.push(asyncRes);
329    const rpc = protos.TraceProcessorRpc.create();
330    rpc.request = TPM.TPM_RESET_TRACE_PROCESSOR;
331    const args = (rpc.resetTraceProcessorArgs =
332      new protos.ResetTraceProcessorArgs());
333    args.dropTrackEventDataBefore = cropTrackEvents
334      ? protos.ResetTraceProcessorArgs.DropTrackEventDataBefore
335          .TRACK_EVENT_RANGE_OF_INTEREST
336      : protos.ResetTraceProcessorArgs.DropTrackEventDataBefore.NO_DROP;
337    args.ingestFtraceInRawTable = ingestFtraceInRawTable;
338    args.analyzeTraceProtoContent = analyzeTraceProtoContent;
339    args.ftraceDropUntilAllCpusValid = ftraceDropUntilAllCpusValid;
340    this.rpcSendRequest(rpc);
341    return asyncRes;
342  }
343
344  // Resets the trace processor state by destroying any table/views created by
345  // the UI after loading.
346  restoreInitialTables(): Promise<void> {
347    const asyncRes = defer<void>();
348    this.pendingRestoreTables.push(asyncRes);
349    const rpc = protos.TraceProcessorRpc.create();
350    rpc.request = TPM.TPM_RESTORE_INITIAL_TABLES;
351    this.rpcSendRequest(rpc);
352    return asyncRes; // Linearize with the worker.
353  }
354
355  // Shorthand for sending a compute metrics request to the engine.
356  async computeMetric(
357    metrics: string[],
358    format: 'json' | 'prototext' | 'proto',
359  ): Promise<string | Uint8Array> {
360    const asyncRes = defer<string | Uint8Array>();
361    this.pendingComputeMetrics.push(asyncRes);
362    const rpc = protos.TraceProcessorRpc.create();
363    rpc.request = TPM.TPM_COMPUTE_METRIC;
364    const args = (rpc.computeMetricArgs = new protos.ComputeMetricArgs());
365    args.metricNames = metrics;
366    if (format === 'json') {
367      args.format = protos.ComputeMetricArgs.ResultFormat.JSON;
368    } else if (format === 'prototext') {
369      args.format = protos.ComputeMetricArgs.ResultFormat.TEXTPROTO;
370    } else if (format === 'proto') {
371      args.format = protos.ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF;
372    } else {
373      throw new Error(`Unknown compute metric format ${format}`);
374    }
375    this.rpcSendRequest(rpc);
376    return asyncRes;
377  }
378
379  // Issues a streaming query and retrieve results in batches.
380  // The returned QueryResult object will be populated over time with batches
381  // of rows (each batch conveys ~128KB of data and a variable number of rows).
382  // The caller can decide whether to wait that all batches have been received
383  // (by awaiting the returned object or calling result.waitAllRows()) or handle
384  // the rows incrementally.
385  //
386  // Example usage:
387  // const res = engine.execute('SELECT foo, bar FROM table');
388  // console.log(res.numRows());  // Will print 0 because we didn't await.
389  // await(res.waitAllRows());
390  // console.log(res.numRows());  // Will print the total number of rows.
391  //
392  // for (const it = res.iter({foo: NUM, bar:STR}); it.valid(); it.next()) {
393  //   console.log(it.foo, it.bar);
394  // }
395  //
396  // Optional |tag| (usually a component name) can be provided to allow
397  // attributing trace processor workload to different UI components.
398  private streamingQuery(
399    sqlQuery: string,
400    tag?: string,
401  ): Promise<QueryResult> & QueryResult {
402    const rpc = protos.TraceProcessorRpc.create();
403    rpc.request = TPM.TPM_QUERY_STREAMING;
404    rpc.queryArgs = new protos.QueryArgs();
405    rpc.queryArgs.sqlQuery = sqlQuery;
406    if (tag) {
407      rpc.queryArgs.tag = tag;
408    }
409    const result = createQueryResult({
410      query: sqlQuery,
411    });
412    this.pendingQueries.push(result);
413    this.rpcSendRequest(rpc);
414    return result;
415  }
416
417  // Wraps .streamingQuery(), captures errors and re-throws with current stack.
418  //
419  // Note: This function is less flexible than .execute() as it only returns a
420  // promise which must be unwrapped before the QueryResult may be accessed.
421  async query(sqlQuery: string, tag?: string): Promise<QueryResult> {
422    try {
423      return await this.streamingQuery(sqlQuery, tag);
424    } catch (e) {
425      // Replace the error's stack trace with the one from here
426      // Note: It seems only V8 can trace the stack up the promise chain, so its
427      // likely this stack won't be useful on !V8.
428      // See
429      // https://docs.google.com/document/d/13Sy_kBIJGP0XT34V1CV3nkWya4TwYx9L3Yv45LdGB6Q
430      captureStackTrace(e);
431      throw e;
432    }
433  }
434
435  async tryQuery(sql: string, tag?: string): Promise<Result<QueryResult>> {
436    try {
437      const result = await this.query(sql, tag);
438      return okResult(result);
439    } catch (error) {
440      const msg = 'message' in error ? `${error.message}` : `${error}`;
441      return errResult(msg);
442    }
443  }
444
445  isMetatracingEnabled(): boolean {
446    return this._isMetatracingEnabled;
447  }
448
449  enableMetatrace(categories?: protos.MetatraceCategories) {
450    const rpc = protos.TraceProcessorRpc.create();
451    rpc.request = TPM.TPM_ENABLE_METATRACE;
452    if (
453      categories !== undefined &&
454      categories !== protos.MetatraceCategories.NONE
455    ) {
456      rpc.enableMetatraceArgs = new protos.EnableMetatraceArgs();
457      rpc.enableMetatraceArgs.categories = categories;
458    }
459    this._isMetatracingEnabled = true;
460    this.rpcSendRequest(rpc);
461  }
462
463  stopAndGetMetatrace(): Promise<protos.DisableAndReadMetatraceResult> {
464    // If we are already finalising a metatrace, ignore the request.
465    if (this.pendingReadMetatrace) {
466      return Promise.reject(new Error('Already finalising a metatrace'));
467    }
468
469    const result = defer<protos.DisableAndReadMetatraceResult>();
470
471    const rpc = protos.TraceProcessorRpc.create();
472    rpc.request = TPM.TPM_DISABLE_AND_READ_METATRACE;
473    this._isMetatracingEnabled = false;
474    this.pendingReadMetatrace = result;
475    this.rpcSendRequest(rpc);
476    return result;
477  }
478
479  registerSqlPackages(pkg: {
480    name: string;
481    modules: {name: string; sql: string}[];
482  }): Promise<void> {
483    if (this.pendingRegisterSqlPackage) {
484      return Promise.reject(new Error('Already finalising a metatrace'));
485    }
486
487    const result = defer<void>();
488
489    const rpc = protos.TraceProcessorRpc.create();
490    rpc.request = TPM.TPM_REGISTER_SQL_PACKAGE;
491    const args = (rpc.registerSqlPackageArgs =
492      new protos.RegisterSqlPackageArgs());
493    args.packageName = pkg.name;
494    args.modules = pkg.modules;
495    args.allowOverride = true;
496    this.pendingRegisterSqlPackage = result;
497    this.rpcSendRequest(rpc);
498    return result;
499  }
500
501  // Marshals the TraceProcessorRpc request arguments and sends the request
502  // to the concrete Engine (Wasm or HTTP).
503  private rpcSendRequest(rpc: protos.TraceProcessorRpc) {
504    rpc.seq = this.txSeqId++;
505    // Each message is wrapped in a TraceProcessorRpcStream to add the varint
506    // preamble with the size, which allows tokenization on the other end.
507    const outerProto = protos.TraceProcessorRpcStream.create();
508    outerProto.msg.push(rpc);
509    const buf = protos.TraceProcessorRpcStream.encode(outerProto).finish();
510    ++this._numRequestsPending;
511    this.rpcSendRequestBytes(buf);
512  }
513
514  get engineId(): string {
515    return this.id;
516  }
517
518  get numRequestsPending(): number {
519    return this._numRequestsPending;
520  }
521
522  getProxy(tag: string): EngineProxy {
523    return new EngineProxy(this, tag);
524  }
525
526  protected fail(reason: string) {
527    this._failed = reason;
528    throw new Error(reason);
529  }
530
531  get failed(): string | undefined {
532    return this._failed;
533  }
534
535  abstract [Symbol.dispose](): void;
536}
537
538// Lightweight engine proxy which annotates all queries with a tag
539export class EngineProxy implements Engine, Disposable {
540  private engine: EngineBase;
541  private tag: string;
542  private _isAlive: boolean;
543
544  constructor(engine: EngineBase, tag: string) {
545    this.engine = engine;
546    this.tag = tag;
547    this._isAlive = true;
548  }
549
550  async query(query: string, tag?: string): Promise<QueryResult> {
551    if (!this._isAlive) {
552      throw new Error(`EngineProxy ${this.tag} was disposed.`);
553    }
554    return await this.engine.query(query, tag);
555  }
556
557  async tryQuery(query: string, tag?: string): Promise<Result<QueryResult>> {
558    if (!this._isAlive) {
559      return errResult(`EngineProxy ${this.tag} was disposed`);
560    }
561    return await this.engine.tryQuery(query, tag);
562  }
563
564  async computeMetric(
565    metrics: string[],
566    format: 'json' | 'prototext' | 'proto',
567  ): Promise<string | Uint8Array> {
568    if (!this._isAlive) {
569      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
570    }
571    return this.engine.computeMetric(metrics, format);
572  }
573
574  enableMetatrace(categories?: protos.MetatraceCategories): void {
575    this.engine.enableMetatrace(categories);
576  }
577
578  stopAndGetMetatrace(): Promise<protos.DisableAndReadMetatraceResult> {
579    return this.engine.stopAndGetMetatrace();
580  }
581
582  get engineId(): string {
583    return this.engine.id;
584  }
585
586  getProxy(tag: string): EngineProxy {
587    return this.engine.getProxy(`${this.tag}/${tag}`);
588  }
589
590  get numRequestsPending() {
591    return this.engine.numRequestsPending;
592  }
593
594  get mode() {
595    return this.engine.mode;
596  }
597
598  get failed() {
599    return this.engine.failed;
600  }
601
602  [Symbol.dispose]() {
603    this._isAlive = false;
604  }
605}
606
607// Capture stack trace and attach to the given error object
608function captureStackTrace(e: Error): void {
609  const stack = new Error().stack;
610  if ('captureStackTrace' in Error) {
611    // V8 specific
612    Error.captureStackTrace(e, captureStackTrace);
613  } else {
614    // Generic
615    Object.defineProperty(e, 'stack', {
616      value: stack,
617      writable: true,
618      configurable: true,
619    });
620  }
621}
622
623// A convenience interface to inject the App in Mithril components.
624export interface EngineAttrs {
625  engine: Engine;
626}
627