1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7/** 8 * @fileoverview TraceEventImporter imports TraceEvent-formatted data 9 * into the provided model. 10 */ 11base.require('base.quad'); 12base.require('tracing.trace_model'); 13base.require('tracing.color_scheme'); 14base.require('tracing.trace_model.instant_event'); 15base.require('tracing.trace_model.counter_series'); 16 17base.exportTo('tracing.importer', function() { 18 19 function deepCopy(value) { 20 if (!(value instanceof Object)) { 21 if (value === undefined || value === null) 22 return value; 23 if (typeof value == 'string') 24 return value.substring(); 25 if (typeof value == 'boolean') 26 return value; 27 if (typeof value == 'number') 28 return value; 29 throw new Error('Unrecognized: ' + typeof value); 30 } 31 32 var object = value; 33 if (object instanceof Array) { 34 var res = new Array(object.length); 35 for (var i = 0; i < object.length; i++) 36 res[i] = deepCopy(object[i]); 37 return res; 38 } 39 40 if (object.__proto__ != Object.prototype) 41 throw new Error('Can only clone simple types'); 42 var res = {}; 43 for (var key in object) { 44 res[key] = deepCopy(object[key]); 45 } 46 return res; 47 } 48 49 function TraceEventImporter(model, eventData) { 50 this.importPriority = 1; 51 this.model_ = model; 52 this.events_ = undefined; 53 this.systemTraceEvents_ = undefined; 54 this.eventsWereFromString_ = false; 55 this.allAsyncEvents_ = []; 56 this.allObjectEvents_ = []; 57 58 if (typeof(eventData) === 'string' || eventData instanceof String) { 59 // If the event data begins with a [, then we know it should end with a ]. 60 // The reason we check for this is because some tracing implementations 61 // cannot guarantee that a ']' gets written to the trace file. So, we are 62 // forgiving and if this is obviously the case, we fix it up before 63 // throwing the string at JSON.parse. 64 if (eventData[0] === '[') { 65 eventData = eventData.replace(/[\r|\n]*$/, '') 66 .replace(/\s*,\s*$/, ''); 67 if (eventData[eventData.length - 1] !== ']') 68 eventData = eventData + ']'; 69 } 70 this.events_ = JSON.parse(eventData); 71 this.eventsWereFromString_ = true; 72 } else { 73 this.events_ = eventData; 74 } 75 76 // Some trace_event implementations put the actual trace events 77 // inside a container. E.g { ... , traceEvents: [ ] } 78 // If we see that, just pull out the trace events. 79 if (this.events_.traceEvents) { 80 var container = this.events_; 81 this.events_ = this.events_.traceEvents; 82 83 // Some trace_event implementations put linux_perf_importer traces as a 84 // huge string inside container.systemTraceEvents. If we see that, pull it 85 // out. It will be picked up by extractSubtrace later on. 86 this.systemTraceEvents_ = container.systemTraceEvents; 87 88 // Any other fields in the container should be treated as metadata. 89 for (var fieldName in container) { 90 if (fieldName === 'traceEvents' || fieldName === 'systemTraceEvents') 91 continue; 92 this.model_.metadata.push({name: fieldName, 93 value: container[fieldName]}); 94 } 95 } 96 } 97 98 /** 99 * @return {boolean} Whether obj is a TraceEvent array. 100 */ 101 TraceEventImporter.canImport = function(eventData) { 102 // May be encoded JSON. But we dont want to parse it fully yet. 103 // Use a simple heuristic: 104 // - eventData that starts with [ are probably trace_event 105 // - eventData that starts with { are probably trace_event 106 // May be encoded JSON. Treat files that start with { as importable by us. 107 if (typeof(eventData) === 'string' || eventData instanceof String) { 108 return eventData[0] == '{' || eventData[0] == '['; 109 } 110 111 // Might just be an array of events 112 if (eventData instanceof Array && eventData.length && eventData[0].ph) 113 return true; 114 115 // Might be an object with a traceEvents field in it. 116 if (eventData.traceEvents) 117 return eventData.traceEvents instanceof Array && 118 eventData.traceEvents[0].ph; 119 120 return false; 121 }; 122 123 TraceEventImporter.prototype = { 124 125 __proto__: Object.prototype, 126 127 extractSubtrace: function() { 128 var tmp = this.systemTraceEvents_; 129 this.systemTraceEvents_ = undefined; 130 return tmp; 131 }, 132 133 /** 134 * Deep copying is only needed if the trace was given to us as events. 135 */ 136 deepCopyIfNeeded_: function(obj) { 137 if (this.eventsWereFromString_) 138 return obj; 139 return deepCopy(obj); 140 }, 141 142 /** 143 * Helper to process an 'async finish' event, which will close an open slice 144 * on a AsyncSliceGroup object. 145 */ 146 processAsyncEvent: function(event) { 147 var thread = this.model_.getOrCreateProcess(event.pid). 148 getOrCreateThread(event.tid); 149 this.allAsyncEvents_.push({ 150 event: event, 151 thread: thread}); 152 }, 153 154 /** 155 * Helper that creates and adds samples to a Counter object based on 156 * 'C' phase events. 157 */ 158 processCounterEvent: function(event) { 159 var ctr_name; 160 if (event.id !== undefined) 161 ctr_name = event.name + '[' + event.id + ']'; 162 else 163 ctr_name = event.name; 164 165 var ctr = this.model_.getOrCreateProcess(event.pid) 166 .getOrCreateCounter(event.cat, ctr_name); 167 168 // Initialize the counter's series fields if needed. 169 if (ctr.numSeries === 0) { 170 for (var seriesName in event.args) { 171 ctr.addSeries(new tracing.trace_model.CounterSeries(seriesName, 172 tracing.getStringColorId(ctr.name + '.' + seriesName))); 173 } 174 175 if (ctr.numSeries === 0) { 176 this.model_.importErrors.push('Expected counter ' + event.name + 177 ' to have at least one argument to use as a value.'); 178 179 // Drop the counter. 180 delete ctr.parent.counters[ctr.name]; 181 return; 182 } 183 } 184 185 var ts = event.ts / 1000; 186 ctr.series.forEach(function(series) { 187 var val = event.args[series.name] ? event.args[series.name] : 0; 188 series.addSample(ts, val); 189 }); 190 }, 191 192 processObjectEvent: function(event) { 193 var thread = this.model_.getOrCreateProcess(event.pid). 194 getOrCreateThread(event.tid); 195 this.allObjectEvents_.push({ 196 event: event, 197 thread: thread}); 198 }, 199 200 processDurationEvent: function(event) { 201 var thread = this.model_.getOrCreateProcess(event.pid) 202 .getOrCreateThread(event.tid); 203 if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(event.ts / 1000)) { 204 this.model_.importErrors.push( 205 'Timestamps are moving backward.'); 206 return; 207 } 208 209 if (event.ph == 'B') { 210 thread.sliceGroup.beginSlice(event.cat, event.name, event.ts / 1000, 211 this.deepCopyIfNeeded_(event.args)); 212 } else { 213 if (!thread.sliceGroup.openSliceCount) { 214 this.model_.importErrors.push( 215 'E phase event without a matching B phase event.'); 216 return; 217 } 218 219 var slice = thread.sliceGroup.endSlice(event.ts / 1000); 220 for (var arg in event.args) { 221 if (slice.args[arg] !== undefined) { 222 this.model_.importErrors.push( 223 'Both the B and E phases of ' + slice.name + 224 'provided values for argument ' + arg + '. ' + 225 'The value of the E phase event will be used.'); 226 } 227 slice.args[arg] = this.deepCopyIfNeeded_(event.args[arg]); 228 } 229 } 230 }, 231 232 processMetadataEvent: function(event) { 233 if (event.name == 'process_name') { 234 var process = this.model_.getOrCreateProcess(event.pid); 235 process.name = event.args.name; 236 } else if (event.name == 'process_labels') { 237 var process = this.model_.getOrCreateProcess(event.pid); 238 process.labels.push.apply( 239 process.labels, event.args.labels.split(',')); 240 } else if (event.name == 'process_sort_index') { 241 var process = this.model_.getOrCreateProcess(event.pid); 242 process.sortIndex = event.args.sort_index; 243 } else if (event.name == 'thread_name') { 244 var thread = this.model_.getOrCreateProcess(event.pid). 245 getOrCreateThread(event.tid); 246 thread.name = event.args.name; 247 } else if (event.name == 'thread_sort_index') { 248 var thread = this.model_.getOrCreateProcess(event.pid). 249 getOrCreateThread(event.tid); 250 thread.sortIndex = event.args.sort_index; 251 } else { 252 this.model_.importErrors.push( 253 'Unrecognized metadata name: ' + event.name); 254 } 255 }, 256 257 // Treat an Instant event as a duration 0 slice. 258 // SliceTrack's redraw() knows how to handle this. 259 processInstantEvent: function(event) { 260 var constructor; 261 switch (event.s) { 262 case 'g': 263 constructor = tracing.trace_model.GlobalInstantEvent; 264 break; 265 case 'p': 266 constructor = tracing.trace_model.ProcessInstantEvent; 267 break; 268 case 't': 269 // fall through 270 default: 271 // Default to thread to support old style input files. 272 constructor = tracing.trace_model.ThreadInstantEvent; 273 break; 274 } 275 276 var colorId = tracing.getStringColorId(event.name); 277 var instantEvent = new constructor(event.cat, event.name, 278 colorId, event.ts / 1000, this.deepCopyIfNeeded_(event.args)); 279 280 switch (instantEvent.type) { 281 case tracing.trace_model.InstantEventType.GLOBAL: 282 this.model_.pushInstantEvent(instantEvent); 283 break; 284 285 case tracing.trace_model.InstantEventType.PROCESS: 286 var process = this.model_.getOrCreateProcess(event.pid); 287 process.pushInstantEvent(instantEvent); 288 break; 289 290 case tracing.trace_model.InstantEventType.THREAD: 291 var thread = this.model_.getOrCreateProcess(event.pid) 292 .getOrCreateThread(event.tid); 293 thread.sliceGroup.pushInstantEvent(instantEvent); 294 break; 295 default: 296 throw new Error('Unknown instant event type: ' + event.s); 297 } 298 }, 299 300 processSampleEvent: function(event) { 301 var thread = this.model_.getOrCreateProcess(event.pid) 302 .getOrCreateThread(event.tid); 303 thread.addSample(event.cat, event.name, event.ts / 1000, 304 this.deepCopyIfNeeded_(event.args)); 305 }, 306 307 /** 308 * Walks through the events_ list and outputs the structures discovered to 309 * model_. 310 */ 311 importEvents: function() { 312 var events = this.events_; 313 for (var eI = 0; eI < events.length; eI++) { 314 var event = events[eI]; 315 if (event.ph === 'B' || event.ph === 'E') { 316 this.processDurationEvent(event); 317 318 } else if (event.ph === 'S' || event.ph === 'F' || event.ph === 'T') { 319 this.processAsyncEvent(event); 320 321 // Note, I is historic. The instant event marker got changed, but we 322 // want to support loading load trace files so we have both I and i. 323 } else if (event.ph == 'I' || event.ph == 'i') { 324 this.processInstantEvent(event); 325 326 } else if (event.ph == 'P') { 327 this.processSampleEvent(event); 328 329 } else if (event.ph == 'C') { 330 this.processCounterEvent(event); 331 332 } else if (event.ph == 'M') { 333 this.processMetadataEvent(event); 334 335 } else if (event.ph === 'N' || event.ph === 'D' || event.ph === 'O') { 336 this.processObjectEvent(event); 337 338 } else if (event.ph === 's' || event.ph === 't' || event.ph === 'f') { 339 // NB: toss flow events until there's proper support 340 341 } else { 342 this.model_.importErrors.push('Unrecognized event phase: ' + 343 event.ph + ' (' + event.name + ')'); 344 } 345 } 346 }, 347 348 /** 349 * Called by the Model after all other importers have imported their 350 * events. 351 */ 352 finalizeImport: function() { 353 this.createAsyncSlices_(); 354 this.createExplicitObjects_(); 355 this.createImplicitObjects_(); 356 }, 357 358 /** 359 * Called by the model to join references between objects, after final model 360 * bounds have been computed. 361 */ 362 joinRefs: function() { 363 this.joinObjectRefs_(); 364 }, 365 366 createAsyncSlices_: function() { 367 if (this.allAsyncEvents_.length == 0) 368 return; 369 370 this.allAsyncEvents_.sort(function(x, y) { 371 return x.event.ts - y.event.ts; 372 }); 373 374 var asyncEventStatesByNameThenID = {}; 375 376 var allAsyncEvents = this.allAsyncEvents_; 377 for (var i = 0; i < allAsyncEvents.length; i++) { 378 var asyncEventState = allAsyncEvents[i]; 379 380 var event = asyncEventState.event; 381 var name = event.name; 382 if (name === undefined) { 383 this.model_.importErrors.push( 384 'Async events (ph: S, T or F) require an name parameter.'); 385 continue; 386 } 387 388 var id = event.id; 389 if (id === undefined) { 390 this.model_.importErrors.push( 391 'Async events (ph: S, T or F) require an id parameter.'); 392 continue; 393 } 394 395 // TODO(simonjam): Add a synchronous tick on the appropriate thread. 396 397 if (event.ph == 'S') { 398 if (asyncEventStatesByNameThenID[name] === undefined) 399 asyncEventStatesByNameThenID[name] = {}; 400 if (asyncEventStatesByNameThenID[name][id]) { 401 this.model_.importErrors.push( 402 'At ' + event.ts + ', a slice of the same id ' + id + 403 ' was alrady open.'); 404 continue; 405 } 406 asyncEventStatesByNameThenID[name][id] = []; 407 asyncEventStatesByNameThenID[name][id].push(asyncEventState); 408 } else { 409 if (asyncEventStatesByNameThenID[name] === undefined) { 410 this.model_.importErrors.push( 411 'At ' + event.ts + ', no slice named ' + name + 412 ' was open.'); 413 continue; 414 } 415 if (asyncEventStatesByNameThenID[name][id] === undefined) { 416 this.model_.importErrors.push( 417 'At ' + event.ts + ', no slice named ' + name + 418 ' with id=' + id + ' was open.'); 419 continue; 420 } 421 var events = asyncEventStatesByNameThenID[name][id]; 422 events.push(asyncEventState); 423 424 if (event.ph == 'F') { 425 // Create a slice from start to end. 426 var slice = new tracing.trace_model.AsyncSlice( 427 events[0].event.cat, 428 name, 429 tracing.getStringColorId(name), 430 events[0].event.ts / 1000); 431 432 slice.duration = (event.ts / 1000) - (events[0].event.ts / 1000); 433 434 slice.startThread = events[0].thread; 435 slice.endThread = asyncEventState.thread; 436 slice.id = id; 437 slice.args = this.deepCopyIfNeeded_(events[0].event.args); 438 slice.subSlices = []; 439 440 // Create subSlices for each step. 441 for (var j = 1; j < events.length; ++j) { 442 var subName = name; 443 if (events[j - 1].event.ph == 'T') 444 subName = name + ':' + events[j - 1].event.args.step; 445 var subSlice = new tracing.trace_model.AsyncSlice( 446 events[0].event.cat, 447 subName, 448 tracing.getStringColorId(name + j), 449 events[j - 1].event.ts / 1000); 450 451 subSlice.duration = 452 (events[j].event.ts / 1000) - (events[j - 1].event.ts / 1000); 453 454 subSlice.startThread = events[j - 1].thread; 455 subSlice.endThread = events[j].thread; 456 subSlice.id = id; 457 subSlice.args = this.deepCopyIfNeeded_(events[j - 1].event.args); 458 459 slice.subSlices.push(subSlice); 460 } 461 462 // The args for the finish event go in the last subSlice. 463 var lastSlice = slice.subSlices[slice.subSlices.length - 1]; 464 for (var arg in event.args) 465 lastSlice.args[arg] = this.deepCopyIfNeeded_(event.args[arg]); 466 467 // Add |slice| to the start-thread's asyncSlices. 468 slice.startThread.asyncSliceGroup.push(slice); 469 delete asyncEventStatesByNameThenID[name][id]; 470 } 471 } 472 } 473 }, 474 475 /** 476 * This function creates objects described via the N, D, and O phase 477 * events. 478 */ 479 createExplicitObjects_: function() { 480 if (this.allObjectEvents_.length == 0) 481 return; 482 483 function processEvent(objectEventState) { 484 var event = objectEventState.event; 485 var thread = objectEventState.thread; 486 if (event.name === undefined) { 487 this.model_.importErrors.push( 488 'While processing ' + JSON.stringify(event) + ': ' + 489 'Object events require an name parameter.'); 490 } 491 492 if (event.id === undefined) { 493 this.model_.importErrors.push( 494 'While processing ' + JSON.stringify(event) + ': ' + 495 'Object events require an id parameter.'); 496 } 497 var process = thread.parent; 498 var ts = event.ts / 1000; 499 var instance; 500 if (event.ph == 'N') { 501 try { 502 instance = process.objects.idWasCreated( 503 event.id, event.cat, event.name, ts); 504 } catch (e) { 505 this.model_.importErrors.push( 506 'While processing create of ' + 507 event.id + ' at ts=' + ts + ': ' + e); 508 return; 509 } 510 } else if (event.ph == 'O') { 511 if (event.args.snapshot === undefined) { 512 this.model_.importErrors.push( 513 'While processing ' + event.id + ' at ts=' + ts + ': ' + 514 'Snapshots must have args: {snapshot: ...}'); 515 return; 516 } 517 var snapshot; 518 try { 519 snapshot = process.objects.addSnapshot( 520 event.id, event.cat, event.name, ts, 521 this.deepCopyIfNeeded_(event.args.snapshot)); 522 } catch (e) { 523 this.model_.importErrors.push( 524 'While processing snapshot of ' + 525 event.id + ' at ts=' + ts + ': ' + e); 526 return; 527 } 528 instance = snapshot.objectInstance; 529 } else if (event.ph == 'D') { 530 try { 531 instance = process.objects.idWasDeleted( 532 event.id, event.cat, event.name, ts); 533 } catch (e) { 534 this.model_.importErrors.push( 535 'While processing delete of ' + 536 event.id + ' at ts=' + ts + ': ' + e); 537 return; 538 } 539 } 540 541 if (instance) 542 instance.colorId = tracing.getStringColorId(instance.typeName); 543 } 544 545 this.allObjectEvents_.sort(function(x, y) { 546 return x.event.ts - y.event.ts; 547 }); 548 549 var allObjectEvents = this.allObjectEvents_; 550 for (var i = 0; i < allObjectEvents.length; i++) { 551 var objectEventState = allObjectEvents[i]; 552 try { 553 processEvent.call(this, objectEventState); 554 } catch (e) { 555 this.model_.importErrors.push(e.message); 556 } 557 } 558 }, 559 560 createImplicitObjects_: function() { 561 base.iterItems(this.model_.processes, function(pid, process) { 562 this.createImplicitObjectsForProcess_(process); 563 }, this); 564 }, 565 566 // Here, we collect all the snapshots that internally contain a 567 // Javascript-level object inside their args list that has an "id" field, 568 // and turn that into a snapshot of the instance referred to by id. 569 createImplicitObjectsForProcess_: function(process) { 570 571 function processField(referencingObject, 572 referencingObjectFieldName, 573 referencingObjectFieldValue, 574 containingSnapshot) { 575 if (!referencingObjectFieldValue) 576 return; 577 578 if (referencingObjectFieldValue.id === undefined) 579 return; 580 if (referencingObjectFieldValue instanceof 581 tracing.trace_model.ObjectSnapshot) 582 return; 583 584 var implicitSnapshot = referencingObjectFieldValue; 585 586 var rawId = implicitSnapshot.id; 587 var m = /(.+)\/(.+)/.exec(rawId); 588 if (!m) 589 throw new Error('Implicit snapshots must have names.'); 590 delete implicitSnapshot.id; 591 var name = m[1]; 592 var id = m[2]; 593 var res; 594 try { 595 res = process.objects.addSnapshot( 596 id, containingSnapshot.objectInstance.category, 597 name, containingSnapshot.ts, 598 implicitSnapshot); 599 } catch (e) { 600 this.model_.importErrors.push( 601 'While processing implicit snapshot of ' + 602 rawId + ' at ts=' + containingSnapshot.ts + ': ' + e); 603 return; 604 } 605 res.objectInstance.hasImplicitSnapshots = true; 606 res.containingSnapshot = containingSnapshot; 607 referencingObject[referencingObjectFieldName] = res; 608 if (!(res instanceof tracing.trace_model.ObjectSnapshot)) 609 throw new Error('Created object must be instanceof snapshot'); 610 return res.args; 611 } 612 613 function iterObject(object, func, containingSnapshot, thisArg) { 614 if (!(object instanceof Object)) 615 return; 616 617 if (object instanceof Array) { 618 for (var i = 0; i < object.length; i++) { 619 var res = func.call(thisArg, object, i, object[i], 620 containingSnapshot); 621 if (res) 622 iterObject(res, func, containingSnapshot, thisArg); 623 else 624 iterObject(object[i], func, containingSnapshot, thisArg); 625 } 626 return; 627 } 628 629 for (var key in object) { 630 var res = func.call(thisArg, object, key, object[key], 631 containingSnapshot); 632 if (res) 633 iterObject(res, func, containingSnapshot, thisArg); 634 else 635 iterObject(object[key], func, containingSnapshot, thisArg); 636 } 637 } 638 639 // TODO(nduca): We may need to iterate the instances in sorted order by 640 // creationTs. 641 process.objects.iterObjectInstances(function(instance) { 642 instance.snapshots.forEach(function(snapshot) { 643 if (snapshot.args.id !== undefined) 644 throw new Error('args cannot have an id field inside it'); 645 iterObject(snapshot.args, processField, snapshot, this); 646 }, this); 647 }, this); 648 }, 649 650 joinObjectRefs_: function() { 651 base.iterItems(this.model_.processes, function(pid, process) { 652 this.joinObjectRefsForProcess_(process); 653 }, this); 654 }, 655 656 joinObjectRefsForProcess_: function(process) { 657 // Iterate the world, looking for id_refs 658 var patchupsToApply = []; 659 base.iterItems(process.threads, function(tid, thread) { 660 thread.asyncSliceGroup.slices.forEach(function(item) { 661 this.searchItemForIDRefs_( 662 patchupsToApply, process.objects, 'start', item); 663 }, this); 664 thread.sliceGroup.slices.forEach(function(item) { 665 this.searchItemForIDRefs_( 666 patchupsToApply, process.objects, 'start', item); 667 }, this); 668 }, this); 669 process.objects.iterObjectInstances(function(instance) { 670 instance.snapshots.forEach(function(item) { 671 this.searchItemForIDRefs_( 672 patchupsToApply, process.objects, 'ts', item); 673 }, this); 674 }, this); 675 676 // Change all the fields pointing at id_refs to their real values. 677 patchupsToApply.forEach(function(patchup) { 678 patchup.object[patchup.field] = patchup.value; 679 }); 680 }, 681 682 searchItemForIDRefs_: function(patchupsToApply, objectCollection, 683 itemTimestampField, item) { 684 if (!item.args) 685 throw new Error(''); 686 687 function handleField(object, fieldName, fieldValue) { 688 if (fieldValue === undefined || 689 (!fieldValue.id_ref && !fieldValue.idRef)) 690 return; 691 692 var id = fieldValue.id_ref || fieldValue.idRef; 693 var ts = item[itemTimestampField]; 694 var snapshot = objectCollection.getSnapshotAt(id, ts); 695 if (!snapshot) 696 return; 697 698 // We have to delay the actual change to the new value until after all 699 // refs have been located. Otherwise, we could end up recursing in 700 // ways we definitely didn't intend. 701 patchupsToApply.push({object: object, 702 field: fieldName, 703 value: snapshot}); 704 } 705 function iterObjectFieldsRecursively(object) { 706 if (!(object instanceof Object)) 707 return; 708 709 if ((object instanceof tracing.trace_model.ObjectSnapshot) || 710 (object instanceof Float32Array) || 711 (object instanceof base.Quad)) 712 return; 713 714 if (object instanceof Array) { 715 for (var i = 0; i < object.length; i++) { 716 handleField(object, i, object[i]); 717 iterObjectFieldsRecursively(object[i]); 718 } 719 return; 720 } 721 722 for (var key in object) { 723 var value = object[key]; 724 handleField(object, key, value); 725 iterObjectFieldsRecursively(value); 726 } 727 } 728 729 iterObjectFieldsRecursively(item.args); 730 } 731 }; 732 733 tracing.TraceModel.registerImporter(TraceEventImporter); 734 735 return { 736 TraceEventImporter: TraceEventImporter 737 }; 738}); 739