[sdk/vm] Use FinalThreadLocal for caching double toString values.

This allows use of double.toString in isolategroup-bound callbacks.

BUG=https://github.com/dart-lang/sdk/issues/61541
TEST=run_isolate_group_run_test

Change-Id: I14443221ffda6f2e639cdbaeea4a2e460a6f42a1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/460960
Commit-Queue: Alexander Aprelev <aam@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
diff --git a/pkg/vm/lib/modular/transformations/deeply_immutable.dart b/pkg/vm/lib/modular/transformations/deeply_immutable.dart
index 5d7c048..0b6bf0b 100644
--- a/pkg/vm/lib/modular/transformations/deeply_immutable.dart
+++ b/pkg/vm/lib/modular/transformations/deeply_immutable.dart
@@ -139,7 +139,7 @@
       }
     }
 
-    if (node.name == 'ScopedThreadLocal') {
+    if ((node.name == 'ScopedThreadLocal' || node.name == 'FinalThreadLocal')) {
       final uri = node.enclosingLibrary.importUri;
       if (uri.isScheme('dart') && uri.path == '_vm') {
         // ScopedThreadLocal has non-deeply-immutable initializer,
diff --git a/sdk/lib/_internal/vm/lib/core_patch.dart b/sdk/lib/_internal/vm/lib/core_patch.dart
index c9c2365..7e266ac 100644
--- a/sdk/lib/_internal/vm/lib/core_patch.dart
+++ b/sdk/lib/_internal/vm/lib/core_patch.dart
@@ -59,6 +59,8 @@
 
 import "dart:typed_data" show Uint8List, Uint16List, Int32List;
 
+import 'dart:_vm' show FinalThreadLocal;
+
 /// These are the additional parts of this patch library:
 part "array.dart";
 part "double.dart";
diff --git a/sdk/lib/_internal/vm/lib/double.dart b/sdk/lib/_internal/vm/lib/double.dart
index 9a8ca17..7633e21 100644
--- a/sdk/lib/_internal/vm/lib/double.dart
+++ b/sdk/lib/_internal/vm/lib/double.dart
@@ -245,34 +245,26 @@
     return this;
   }
 
-  static const int CACHE_SIZE_LOG2 = 3;
-  static const int CACHE_LENGTH = 1 << (CACHE_SIZE_LOG2 + 1);
-  static const int CACHE_MASK = CACHE_LENGTH - 1;
   // Each key (double) followed by its toString result.
-  static final List _cache = List.filled(CACHE_LENGTH, null);
-  static int _cacheEvictIndex = 0;
+  @pragma("vm:shared")
+  static final _cacheThreadLocal = FinalThreadLocal<_DoubleToStringCache>(
+    () => _DoubleToStringCache(),
+  );
 
   @pragma("vm:external-name", "Double_toString")
   external String _toString();
 
   String toString() {
-    // TODO(koda): Consider starting at most recently inserted.
-    for (int i = 0; i < CACHE_LENGTH; i += 2) {
-      // Need 'identical' to handle negative zero, etc.
-      if (identical(_cache[i], this)) {
-        return _cache[i + 1];
-      }
+    final cache = _cacheThreadLocal.value;
+    final cachedValue = cache.lookup(this);
+    if (cachedValue != null) {
+      return cachedValue;
     }
     // TODO(koda): Consider optimizing all small integral values.
     if (identical(0.0, this)) {
       return "0.0";
     }
-    String result = _toString();
-    // Replace the least recently inserted entry.
-    _cache[_cacheEvictIndex] = this;
-    _cache[_cacheEvictIndex + 1] = result;
-    _cacheEvictIndex = (_cacheEvictIndex + 2) & CACHE_MASK;
-    return result;
+    return cache.store(this, _toString());
   }
 
   String toStringAsFixed(int fractionDigits) {
@@ -407,3 +399,32 @@
     }
   }
 }
+
+class _DoubleToStringCache {
+  static const int _CACHE_SIZE_LOG2 = 3;
+  static const int _CACHE_LENGTH = 1 << (_CACHE_SIZE_LOG2 + 1);
+  static const int _CACHE_MASK = _CACHE_LENGTH - 1;
+
+  // Each key (double) followed by its toString result.
+  final List list = List.filled(_CACHE_LENGTH, null);
+  int evictIndex = 0;
+
+  String? lookup(double v) {
+    // TODO(koda): Consider starting at most recently inserted.
+    for (int i = 0; i < _CACHE_LENGTH; i += 2) {
+      // Need 'identical' to handle negative zero, etc.
+      if (identical(list[i], v)) {
+        return list[i + 1];
+      }
+    }
+    return null;
+  }
+
+  String store(double v, String s) {
+    // Replace the least recently inserted entry.
+    list[evictIndex] = v;
+    list[evictIndex + 1] = s;
+    evictIndex = (evictIndex + 2) & _CACHE_MASK;
+    return s;
+  }
+}
diff --git a/sdk/lib/_vm/_vm.dart b/sdk/lib/_vm/_vm.dart
index 5358c6c..5ba32baa 100644
--- a/sdk/lib/_vm/_vm.dart
+++ b/sdk/lib/_vm/_vm.dart
@@ -8,68 +8,34 @@
 
 @pragma("vm:deeply-immutable")
 @pragma('vm:entry-point')
-final class ScopedThreadLocal<T> {
-  /// Creates scoped thread local value with the given [initializer] function.
-  ///
-  /// [initializer] must be trivially shareable.
-  ScopedThreadLocal([this._initializer]) : _id = _allocateId();
+final class ThreadLocal<T> {
+  /// Creates dart thread local variable.
+  ThreadLocal() : _id = _allocateId();
 
-  /// Execute [f] binding this [ScopedThreadLocal] to the given
-  /// [value] for the duration of the execution.
-  R runWith<R>(T value, R Function(T) f) {
-    bool hadValue = _hasValue(_id);
-    Object? previous_value = hadValue ? _getValue(_id) : null;
-    _setValue(_id, value);
-    R result = f(value);
-    if (hadValue) {
-      _setValue(_id, previous_value!);
-    } else {
-      _clearValue(_id);
-    }
-    return result;
-  }
-
-  /// Execute [f] initializing this [ScopedThreadLocal] using default initializer if needed.
-  /// Throws [StateError] if this [ScopedThreadLocal] does not have an initializer.
-  R runInitialized<R>(R Function(T) f) {
-    bool hadValue = _hasValue(_id);
-    Object? previous_value = hadValue ? _getValue(_id) : null;
-    late T v;
-    if (!isBound) {
-      if (_initializer == null) {
-        throw StateError(
-          "No initializer was provided for this ScopedThreadLocal.",
-        );
-      }
-      v = _initializer!();
-      _setValue(_id, v);
-    } else {
-      v = unsafeCast<T>(_getValue(_id));
-    }
-    R result = f(v);
-    if (hadValue) {
-      _setValue(_id, previous_value!);
-    } else {
-      _clearValue(_id);
-    }
-    return result;
-  }
-
-  /// Returns the value specified by the closest enclosing invocation of
-  /// [runWith] or [runInititalized] or throws [StateError] if this
-  /// [ScopedThreadLocal] is not bound to a value.
+  /// Returns the value of this thread-local variable or throws [StateError]
+  /// if it has no value.
   T get value {
     if (!_hasValue(_id)) {
       throw StateError(
-        "Attempt to access value that was not bound. "
-        "Use runInititalized or runWith.",
+        "Attempt to access variable that was not assigned a value.",
       );
     }
     return unsafeCast<T>(_getValue(_id));
   }
 
-  /// Returns `true` if this [ScopedThreadLocal] is bound to a value.
-  bool get isBound => _hasValue(_id);
+  /// Sets the value of this variable. Overwrites old value if it was previously
+  /// set.
+  set value(T newValue) {
+    _setValue(_id, newValue);
+  }
+
+  /// Returns `true` if some value was assigned to this variable.
+  bool get hasValue => _hasValue(_id);
+
+  // Clears this variable of its assigned value.
+  void clearValue() {
+    _clearValue(_id);
+  }
 
   @pragma("vm:external-name", "ScopedThreadLocal_allocateId")
   external static int _allocateId();
@@ -87,5 +53,89 @@
   external static void _clearValue(int id);
 
   final int _id;
+}
+
+@pragma("vm:deeply-immutable")
+@pragma('vm:entry-point')
+final class ScopedThreadLocal<T> {
+  /// Creates scoped thread-local variable with given [initializer] function.
+  ///
+  /// [initializer] must be trivially shareable.
+  ScopedThreadLocal([this._initializer]);
+
+  /// Execute [f] binding this [ScopedThreadLocal] to the given
+  /// [value] for the duration of the execution.
+  R runWith<R>(T new_value, R Function(T) f) {
+    bool had_value = variable.hasValue;
+    T? previous_value = had_value ? variable.value : null;
+    variable.value = new_value;
+    R result = f(new_value);
+    if (had_value) {
+      variable.value = previous_value as T;
+    } else {
+      variable.clearValue();
+    }
+    return result;
+  }
+
+  /// Execute [f] initializing this [ScopedThreadLocal] using default initializer if needed.
+  /// Throws [StateError] if this [ScopedThreadLocal] does not have an initializer.
+  R runInitialized<R>(R Function(T) f) {
+    bool had_value = variable.hasValue;
+    T? previous_value = had_value ? variable.value : null;
+    if (!variable.hasValue) {
+      if (_initializer == null) {
+        throw StateError(
+          "No initializer was provided for this ScopedThreadLocal.",
+        );
+      }
+      variable.value = _initializer!();
+    }
+    R result = f(variable.value);
+    if (had_value) {
+      variable.value = previous_value as T;
+    } else {
+      variable.clearValue();
+    }
+    return result;
+  }
+
+  /// Returns the value specified by the closest enclosing invocation of
+  /// [runWith] or [runInititalized] or throws [StateError] if this
+  /// [ScopedThreadLocal] is not bound to a value.
+  T get value => variable.value;
+
+  /// Returns `true` if this [ScopedThreadLocal] is bound to a value.
+  bool get isBound => variable.hasValue;
   final T Function()? _initializer;
+
+  final variable = ThreadLocal<T>();
+}
+
+@pragma("vm:deeply-immutable")
+@pragma('vm:entry-point')
+final class FinalThreadLocal<T> {
+  /// Creates thread local value with the given [initializer] function.
+  ///
+  /// The value can be assigned only once, remains assigned for the duration
+  /// of this dart thread lifetime.
+  ///
+  /// [initializer] must be trivially shareable.
+  FinalThreadLocal(this._initializer);
+
+  /// Returns the value bound to [FinalThreadLocal].
+  T get value {
+    if (!variable.hasValue) {
+      variable.value = _initializer();
+    }
+    return variable.value;
+  }
+
+  set value(_) {
+    throw StateError("Final value can not be updated");
+  }
+
+  final T Function() _initializer;
+
+  final variable = ThreadLocal<T>();
 }
diff --git a/tests/ffi/run_isolate_group_run_test.dart b/tests/ffi/run_isolate_group_run_test.dart
index 1e2e7f7..d82e3d3 100644
--- a/tests/ffi/run_isolate_group_run_test.dart
+++ b/tests/ffi/run_isolate_group_run_test.dart
@@ -82,6 +82,9 @@
 @pragma('vm:shared')
 String default_tag = "";
 
+@pragma('vm:shared')
+double pi = 3.14159;
+
 main(List<String> args) {
   IsolateGroup.runSync(() {
     final l = <int>[];
@@ -247,5 +250,14 @@
   });
   Expect.notEquals("", default_tag);
 
+  final result = IsolateGroup.runSync(() {
+    return pi.toString();
+  });
+  Expect.equals("3.14159", result);
+  final resultIdentical = IsolateGroup.runSync(() {
+    return identical(pi.toString(), pi.toString());
+  });
+  Expect.isTrue(resultIdentical);
+
   print("All tests completed :)");
 }