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