// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // Patch file for dart:developer library. import 'dart:_js_helper' show ForceInline, ReifyFunctionTypes; import 'dart:_foreign_helper' show JS, JSExportName; import 'dart:_internal' show patch; import 'dart:_runtime' as dart; import 'dart:async'; import 'dart:convert' show json; import 'dart:isolate'; // These values must be kept in sync with developer/timeline.dart. const int _beginPatch = 1; const int _endPatch = 2; const int _asyncBeginPatch = 5; const int _asyncEndPatch = 7; const int _flowBeginPatch = 9; const int _flowStepPatch = 10; const int _flowEndPatch = 11; var _issuedRegisterExtensionWarning = false; var _issuedPostEventWarning = false; final _developerSupportWarning = 'from dart:developer is only supported in ' 'build/run/test environments where the developer event method hooks have ' 'been set by package:dwds v11.1.0 or higher.'; /// Returns `true` if the debugger service has been attached to the app. // TODO(46377) Update this check when we have a documented API for DDC apps. bool get _debuggerAttached => JS('!', r'!!#.$dwdsVersion', dart.global_); @patch @ForceInline() bool debugger({bool when = true, String? message}) { if (when) { JS('', 'debugger'); } return when; } @patch Object? inspect(Object? object) { // Note: this log level does not show up by default in Chrome. // This is used for communication with the debugger service. JS('', 'console.debug("dart.developer.inspect", #)', object); return object; } @patch void log( String message, { DateTime? time, int? sequenceNumber, int level = 0, String name = '', Zone? zone, Object? error, StackTrace? stackTrace, }) { Object items = JS( '!', '{ message: #, name: #, level: # }', message, name, level, ); if (time != null) JS('', '#.time = #', items, time); if (sequenceNumber != null) { JS('', '#.sequenceNumber = #', items, sequenceNumber); } if (zone != null) JS('', '#.zone = #', items, zone); if (error != null) JS('', '#.error = #', items, error); if (stackTrace != null) JS('', '#.stackTrace = #', items, stackTrace); JS('', 'console.debug("dart.developer.log", #)', items); } @patch int get reachabilityBarrier => 0; final _extensions = {}; @patch ServiceExtensionHandler? _lookupExtension(String method) { return _extensions[method]; } @patch _registerExtension(String method, ServiceExtensionHandler handler) { if (!_debuggerAttached) { if (!_issuedRegisterExtensionWarning) { var message = 'registerExtension() $_developerSupportWarning'; JS('', 'console.warn(#)', message); _issuedRegisterExtensionWarning = true; } return; } // TODO(46377) Update this check when we have a documented API for DDC apps. if (JS('!', r'!!#.$emitRegisterEvent', dart.global_)) { _extensions[method] = handler; // See hooks assigned by package:dwds: // https://github.com/dart-lang/webdev/blob/de05cf9fbbfe088be74bb61df4a138289a94d902/dwds/web/client.dart#L223 JS('', r'#.$emitRegisterEvent(#)', dart.global_, method); } } /// Returns a JS `Promise` that resolves with the result of invoking /// [methodName] with an [encodedJson] map as its parameters. /// /// This is used by the VM Service Protocol to invoke extensions registered /// with [registerExtension]. For example, in JS: /// /// await dartDevEmbedder.developer.invokeExtension( /// "ext.flutter.inspector.getRootWidget", '{"objectGroup":""}'); /// @JSExportName('invokeExtension') @ReifyFunctionTypes(false) _invokeExtension(String methodName, String encodedJson) { // TODO(vsm): We should factor this out as future<->promise. return JS('', 'new #.Promise(#)', dart.global_, ( Function(Object) resolve, Function(Object) reject, ) async { try { var method = _lookupExtension(methodName)!; var parameters = (json.decode(encodedJson) as Map).cast(); var result = await method(methodName, parameters); resolve(result._toString()); } catch (e) { // TODO(vsm): Reject or encode in result? reject('$e'); } }); } @patch bool get extensionStreamHasListener => _debuggerAttached; @patch void _postEvent(String eventKind, String eventData) { if (!_debuggerAttached) { if (!_issuedPostEventWarning) { var message = 'postEvent() $_developerSupportWarning'; JS('', 'console.warn(#)', message); _issuedPostEventWarning = true; } return; } // TODO(46377) Update this check when we have a documented API for DDC apps. if (JS('!', r'!!#.$emitDebugEvent', dart.global_)) { // See hooks assigned by package:dwds: // https://github.com/dart-lang/webdev/blob/de05cf9fbbfe088be74bb61df4a138289a94d902/dwds/web/client.dart#L220 JS('', r'#.$emitDebugEvent(#, #)', dart.global_, eventKind, eventData); } } @patch bool _isDartStreamEnabled() { return true; } @patch int _getTraceClock() { // Note: Use `millisecondsSinceEpoch` instead of `microsecondsSinceEpoch` // because JS isn't able to hold the value of `microsecondsSinceEpoch` without // rounding errors. return DateTime.now().millisecondsSinceEpoch; } // The sum of the number of "mark" `PerformanceEntry`s and the number of // "measure" `PerformanceEntry`s in the browser. int _markAndMeasureEntryCount = 0; // The target upper bound on [_markAndMeasureEntryCount]. const int _markAndMeasureEntryCountLimit = 10000; int _taskId = 1; @patch int _getNextTaskId() { return _taskId++; } bool _isBeginEvent(int type) => type == _beginPatch || type == _asyncBeginPatch; bool _isEndEvent(int type) => type == _endPatch || type == _asyncEndPatch; bool _isUnsupportedEvent(int type) => type == _flowBeginPatch || type == _flowEndPatch || type == _flowStepPatch; String _createEventName({ required int taskId, required String name, required bool isBeginEvent, required bool isEndEvent, }) { if (isBeginEvent) { return '$taskId-$name-begin'; } if (isEndEvent) { return '$taskId-$name-end'; } // Return only the name for events that don't need measurements: return name; } Map _eventNameToCount = {}; String _postfixWithCount(String eventName) { final count = _eventNameToCount[eventName]; if (count == null) return eventName; return '$eventName-$count'; } void _incrementEventCount(String eventName) { final currentCount = _eventNameToCount[eventName] ?? 0; _eventNameToCount[eventName] = currentCount + 1; } void _decrementEventCount(String eventName) { if (!_eventNameToCount.containsKey(eventName)) return; final newCount = _eventNameToCount[eventName]! - 1; if (newCount <= 0) { _eventNameToCount.remove(eventName); } else { _eventNameToCount[eventName] = newCount; } } bool get _areAllBeginEventsPaired => _eventNameToCount.isEmpty; @patch void _reportTaskEvent( int taskId, int flowId, int type, String name, String argumentsAsJson, ) { // Ignore any unsupported events. if (_isUnsupportedEvent(type)) return; // Enforce the upper bound on the sum of the number of "mark" // `PerformanceEntry`s and the number of "measure" `PerformanceEntry`s to the // best of our ability. Sometimes the upper bound may be exceeded because we // cannot clear begin events that haven't yet been paired with end events. // `PerformanceEntry`s can only be seen by a user after they record a // performance profile that includes the entries, so our clearing of // `PerformanceEntry`s will never have a negative effect on a user's profiling // experience. For any cleared set of entries: either 1) the user was // recording and captured those entries in a profile, or 2) the user was not // recording and missed their chance to capture those entries, as even if // those entries weren't deleted, it wouldn't be possible to retroactively add // them to a profile. if (_markAndMeasureEntryCount > _markAndMeasureEntryCountLimit && _areAllBeginEventsPaired) { JS('', 'performance.clearMarks()'); JS('', 'performance.clearMeasures()'); _markAndMeasureEntryCount = 0; } final isBeginEvent = _isBeginEvent(type); final isEndEvent = _isEndEvent(type); var currentEventName = _createEventName( taskId: taskId, name: name, isBeginEvent: isBeginEvent, isEndEvent: isEndEvent, ); // Postfix the event name with the current count of events with that name. This // guarantees that we are always measuring from the most recent begin event. if (isBeginEvent) { _incrementEventCount(currentEventName); currentEventName = _postfixWithCount(currentEventName); } final markOptions = JS('', '{detail: JSON.parse(#)}', argumentsAsJson); // Start by creating a mark event. JS('', 'performance.mark(#, #)', currentEventName, markOptions); _markAndMeasureEntryCount++; // If it's an end event, then create a measurement from the most recent begin // event with the same name. if (isEndEvent) { final beginEventName = _createEventName( taskId: taskId, name: name, isBeginEvent: true, isEndEvent: false, ); JS( '', 'performance.measure(#, #, #)', name, _postfixWithCount(beginEventName), currentEventName, ); _markAndMeasureEntryCount++; _decrementEventCount(beginEventName); } _markAndMeasureEntryCount.clamp(0, _markAndMeasureEntryCountLimit + 1); } @patch int _getServiceMajorVersion() { return 0; } @patch int _getServiceMinorVersion() { return 0; } @patch void _getServerInfo(SendPort sendPort) { sendPort.send(null); } @patch void _webServerControl(SendPort sendPort, bool enable, bool? silenceOutput) { sendPort.send(null); } @patch String? _getIsolateIdFromSendPort(SendPort sendPort) { return null; } @patch String? _getObjectId(Object object) { return null; } @patch class UserTag { @patch factory UserTag(String label) = _FakeUserTag; @patch static UserTag get defaultTag => _FakeUserTag._defaultTag; } final class _FakeUserTag implements UserTag { static final _instances = {}; _FakeUserTag.real(this.label); factory _FakeUserTag(String label) { // Canonicalize by name. var existingTag = _instances[label]; if (existingTag != null) { return existingTag; } // Throw an exception if we've reached the maximum number of user tags. if (_instances.length == UserTag.maxUserTags) { throw UnsupportedError( 'UserTag instance limit (${UserTag.maxUserTags}) reached.', ); } return _instances[label] = _FakeUserTag.real(label); } final String label; UserTag makeCurrent() { var old = _currentTag; _currentTag = this; return old; } static final UserTag _defaultTag = _FakeUserTag('Default'); } var _currentTag = _FakeUserTag._defaultTag; @patch UserTag getCurrentTag() => _currentTag; @patch abstract final class NativeRuntime { @patch static String? get buildId => null; @patch static void writeHeapSnapshotToFile(String filepath) => throw UnsupportedError( "Generating heap snapshots is not supported on the web.", ); }