Bug 1896505 - Add initial reporting machinery for subclassing. r=jandem

This patch will report the Type II Array subclassing.

A previous version of this patch had CacheIR support for this; however this has
proved to have less performance impact and more complexity challenges than
forseen. As a result, this support has been removed until proven necessary.

Differential Revision: https://phabricator.services.mozilla.com/D210535
This commit is contained in:
Matthew Gaudet 2024-05-29 16:28:36 +00:00
parent 062031b254
commit 9f2510928b
12 changed files with 247 additions and 22 deletions

View File

@ -70,6 +70,7 @@ custom onunderflow sets an element onunderflow event listener
custom JS_asmjs uses asm.js
custom JS_wasm uses WebAssembly
custom JS_wasm_legacy_exceptions uses WebAssembly legacy exception-handling
custom JS_subclassing_array_type_2 Array is Type II subclassed
// Console API
method console.assert

View File

@ -107,8 +107,8 @@ use.counter:
send_in_pings:
- use-counters
# Total of 2309 use counter metrics (excludes denominators).
# Total of 354 'page' use counters.
# Total of 2311 use counter metrics (excludes denominators).
# Total of 355 'page' use counters.
use.counter.page:
svgsvgelement_getelementbyid:
type: counter
@ -535,6 +535,23 @@ use.counter.page:
send_in_pings:
- use-counters
js_subclassing_array_type_2:
type: counter
description: >
Whether a page Array is Type II subclassed.
Compare against `use.counter.top_level_content_documents_destroyed`
to calculate the rate.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
console_assert:
type: counter
description: >
@ -6128,7 +6145,7 @@ use.counter.page:
send_in_pings:
- use-counters
# Total of 354 'document' use counters.
# Total of 355 'document' use counters.
use.counter.doc:
svgsvgelement_getelementbyid:
type: counter
@ -6555,6 +6572,23 @@ use.counter.doc:
send_in_pings:
- use-counters
js_subclassing_array_type_2:
type: counter
description: >
Whether a document Array is Type II subclassed.
Compare against `use.counter.content_documents_destroyed`
to calculate the rate.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
console_assert:
type: counter
description: >
@ -39378,3 +39412,4 @@ use.counter.css.doc:
expires: never
send_in_pings:
- use-counters

View File

@ -84,10 +84,11 @@ using JSAccumulateTelemetryDataCallback = void (*)(JSMetric, uint32_t);
extern JS_PUBLIC_API void JS_SetAccumulateTelemetryCallback(
JSContext* cx, JSAccumulateTelemetryDataCallback callback);
#define FOR_EACH_JS_USE_COUNTER(_) \
_(ASMJS, AsmJS) \
_(WASM, Wasm) \
_(WASM_LEGACY_EXCEPTIONS, WasmLegacyExceptions)
#define FOR_EACH_JS_USE_COUNTER(_) \
_(ASMJS, AsmJS) \
_(WASM, Wasm) \
_(WASM_LEGACY_EXCEPTIONS, WasmLegacyExceptions) \
_(SUBCLASSING_ARRAY_TYPE_II, SubclassingArrayTypeII)
/*
* Use counter names passed to the accumulate use counter callback.

View File

@ -22,6 +22,7 @@
#include "jsnum.h"
#include "jstypes.h"
#include "builtin/SelfHostingDefines.h"
#include "ds/Sort.h"
#include "jit/InlinableNatives.h"
#include "jit/TrampolineNatives.h"
@ -958,7 +959,7 @@ static SharedShape* AddLengthProperty(JSContext* cx,
map, mapLength, objectFlags);
}
static bool IsArrayConstructor(const JSObject* obj) {
bool js::IsArrayConstructor(const JSObject* obj) {
// Note: this also returns true for cross-realm Array constructors in the
// same compartment.
return IsNativeFunction(obj, ArrayConstructor);
@ -4249,6 +4250,11 @@ static bool array_of(JSContext* cx, unsigned argc, Value* vp) {
return ArrayFromCallArgs(cx, args);
}
if (!ReportUsageCounter(cx, nullptr, SUBCLASSING_ARRAY,
SUBCLASSING_TYPE_II)) {
return false;
}
// Step 4.
RootedObject obj(cx);
{

View File

@ -257,6 +257,8 @@ class MOZ_NON_TEMPORARY_CLASS ArraySpeciesLookup final {
}
};
bool IsArrayConstructor(const JSObject* obj);
} /* namespace js */
#endif /* builtin_Array_h */

View File

@ -681,7 +681,8 @@ function ArrayFromAsync(asyncItems, mapfn = undefined, thisArg = undefined) {
// Step 3.e.i. Let A be ? Construct(C).
// Step 3.f. Else,
// Step 3.f.i. Let A be ! ArrayCreate(0).
var A = IsConstructor(C) ? constructContentFunction(C, C) : [];
var A = IsConstructor(C) ?
(ReportUsageCounter(C, SUBCLASS_ARRAY_TYPE_II), constructContentFunction(C, C)) : [];
// Step 3.j.i. Let k be 0.
@ -750,7 +751,7 @@ function ArrayFromAsync(asyncItems, mapfn = undefined, thisArg = undefined) {
// Step 3.k.iv.1. Let A be ? Construct(C, « 𝔽(len) »).
// Step 3.k.v. Else,
// Step 3.k.v.1. Let A be ? ArrayCreate(len).
var A = IsConstructor(C) ? constructContentFunction(C, C, len) : std_Array(len);
var A = IsConstructor(C) ? (ReportUsageCounter(C, SUBCLASS_ARRAY_TYPE_II), constructContentFunction(C, C, len)) : std_Array(len);
// Step 3.k.vi. Let k be 0.
var k = 0;
@ -814,7 +815,7 @@ function ArrayFrom(items, mapfn = undefined, thisArg = undefined) {
}
// Steps 5.a-b.
var A = IsConstructor(C) ? constructContentFunction(C, C) : [];
var A = IsConstructor(C) ? (ReportUsageCounter(C, SUBCLASS_ARRAY_TYPE_II), constructContentFunction(C, C)) : [];
// Step 5.d.
var k = 0;
@ -857,7 +858,7 @@ function ArrayFrom(items, mapfn = undefined, thisArg = undefined) {
// Steps 12-14.
var A = IsConstructor(C)
? constructContentFunction(C, C, len)
? (ReportUsageCounter(C, SUBCLASS_ARRAY_TYPE_II), constructContentFunction(C, C, len))
: std_Array(len);
// Steps 15-16.
@ -921,7 +922,7 @@ function ArrayToLocaleString(locales, options) {
if (IsNullOrUndefined(firstElement)) {
R = "";
} else {
#if JS_HAS_INTL_API
#if JS_HAS_INTL_API
R = ToString(
callContentFunction(
firstElement.toLocaleString,
@ -930,11 +931,11 @@ function ArrayToLocaleString(locales, options) {
options
)
);
#else
#else
R = ToString(
callContentFunction(firstElement.toLocaleString, firstElement)
);
#endif
#endif
}
// Step 3 (reordered).
@ -949,7 +950,7 @@ function ArrayToLocaleString(locales, options) {
// Steps 9.a, 9.c-e.
R += separator;
if (!IsNullOrUndefined(nextElement)) {
#if JS_HAS_INTL_API
#if JS_HAS_INTL_API
R += ToString(
callContentFunction(
nextElement.toLocaleString,
@ -958,11 +959,11 @@ function ArrayToLocaleString(locales, options) {
options
)
);
#else
#else
R += ToString(
callContentFunction(nextElement.toLocaleString, nextElement)
);
#endif
#endif
}
}

View File

@ -128,4 +128,18 @@
#define ASYNC_ITERATOR_HELPER_GENERATOR_SLOT 0
// Support for usage counters around subclassing:
#define SUBCLASSING_ARRAY 1
#define SUBCLASSING_LAST_BUILTIN 2
#define SUBCLASSING_TYPE_II 2
#define SUBCLASSING_TYPE_III 3
#define SUBCLASSING_TYPE_IV 4
#define SUBCLASSING_TYPE_MASK 0xf
#define SUBCLASSING_BUILTIN_SHIFT 16
#define SUBCLASS_ARRAY_TYPE_II \
((SUBCLASSING_ARRAY << SUBCLASSING_BUILTIN_SHIFT) | SUBCLASSING_TYPE_II)
#endif

View File

@ -0,0 +1,57 @@
function test_function_for_use_counter_integration(fn, counter, expected_growth = true) {
let before = getUseCounterResults();
assertEq(counter in before, true);
fn();
let after = getUseCounterResults();
if (expected_growth) {
console.log("Yes Increase: Before ", before[counter], " After", after[counter]);
assertEq(after[counter] > before[counter], true);
} else {
console.log("No Increase: Before ", before[counter], " After", after[counter]);
assertEq(after[counter] == before[counter], true);
}
}
class MyArray extends Array { }
function array_from() {
let r = Array.from([1, 2, 3]);
assertEq(r instanceof Array, true);
}
function array_from_subclassing_type_ii() {
assertEq(MyArray.from([1, 2, 3]) instanceof MyArray, true);
}
test_function_for_use_counter_integration(array_from, "SubclassingArrayTypeII", /* expected_growth = */ false);
test_function_for_use_counter_integration(array_from_subclassing_type_ii, "SubclassingArrayTypeII", /* expected_growth = */ true);
function array_of() {
let r = Array.of([1, 2, 3]);
assertEq(r instanceof Array, true);
}
function array_of_subclassing_type_ii() {
assertEq(MyArray.of([1, 2, 3]) instanceof MyArray, true);
}
test_function_for_use_counter_integration(array_of, "SubclassingArrayTypeII", /* expected_growth = */ false);
test_function_for_use_counter_integration(array_of_subclassing_type_ii, "SubclassingArrayTypeII", /* expected_growth = */ true);
// Array.fromAsync
function array_fromAsync() {
let r = Array.fromAsync([1, 2, 3]);
r.then((x) => assertEq(x instanceof Array, true));
}
function array_fromAsync_subclassing_type_ii() {
MyArray.fromAsync([1, 2, 3]).then((x) => assertEq(x instanceof MyArray, true));
}
test_function_for_use_counter_integration(array_fromAsync, "SubclassingArrayTypeII", /* expected_growth = */ false);
test_function_for_use_counter_integration(array_fromAsync_subclassing_type_ii, "SubclassingArrayTypeII", /* expected_growth = */ true);
// Array.of

View File

@ -0,0 +1,20 @@
let countersBefore = getUseCounterResults();
class MyArray extends Array { }
function f() { return MyArray.from([1, 2, 3]) };
assertEq(f() instanceof MyArray, true);
for (var i = 0; i < 100; i++) { f(); }
let countersAfter = getUseCounterResults();
// The above code should have tripped the subclassing detection.
assertEq(countersAfter.SubclassingArrayTypeII > countersBefore.SubclassingArrayTypeII, true);
function f2() {
return Array.from([1, 2, 3]);
}
f2();
let countersAfterNoChange = getUseCounterResults();
assertEq(countersAfter.SubclassingArrayTypeII, countersAfterNoChange.SubclassingArrayTypeII);

View File

@ -2161,6 +2161,7 @@ static const JSFunctionSpec intrinsic_functions[] = {
JS_INLINABLE_FN("RegExpSearcher", RegExpSearcher, 3, 0, RegExpSearcher),
JS_INLINABLE_FN("RegExpSearcherLastLimit", RegExpSearcherLastLimit, 0, 0,
RegExpSearcherLastLimit),
JS_FN("ReportUsageCounter", intrinsic_ReportUsageCounter, 2, 0),
JS_INLINABLE_FN("SameValue", js::obj_is, 2, 0, ObjectIs),
JS_FN("SetCopy", SetObject::copy, 1, 0),
JS_FN("SharedArrayBufferByteLength",
@ -2960,6 +2961,82 @@ bool js::IsSelfHostedFunctionWithName(const Value& v, JSAtom* name) {
return IsSelfHostedFunctionWithName(fun, name);
}
bool js::ReportUsageCounter(JSContext* cx, HandleObject constructor,
int32_t builtin, int32_t type) {
switch (builtin) {
case SUBCLASSING_ARRAY: {
// Check if the provided function is actually the array constructor
// anyhow; We're interested in if the object is in the current realm; CCW
// realm is OK because even if this is a CCW it'll fail the
// IsArrayConstructor check.
//
// Constructor may be nullptr if check has already been done.
if (constructor && IsArrayConstructor(constructor) &&
constructor->maybeCCWRealm() == cx->realm()) {
return true;
}
switch (type) {
case SUBCLASSING_TYPE_II:
cx->runtime()->setUseCounter(cx->global(),
JSUseCounter::SUBCLASSING_ARRAY_TYPE_II);
return true;
default:
MOZ_CRASH("Unexpected Subclassing Type");
}
}
default:
MOZ_CRASH("Unexpected builtin");
};
}
// In the interests of efficiency and simplicity, we would like this function
// to have to do as little as possible, and take as few parameters as possible.
//
// Nevethreless, we also wish to be able to report correctly for interesting
// cases like Array.from.call(Map, ...) -- essentially, catching the case where
// someone using a subclassing-elgible class as the 'this' value, thereby
// executing a sub classing.
//
// To handle this with just two parameters, we treat our integer parameter as a
// packed integer; This introduces some magic, but allows us to communicate all
// call-site constant data in a single int32.
//
// The packing is as follows:
//
// SubclassingElgibleBuiltins << 16 | SubclassingType
//
// This produces the following magic constant values:
//
// Array Subclassing Type II: (1 << 16) | 2 == 0x010002
// Array Subclassing Type III: (1 << 16) | 3 == 0x010003
// Array Subclassing Type IV: (1 << 16) | 4 == 0x010004
//
// Subclassing is reported iff the constructor provided doesn't match
// the existing prototype.
//
bool js::intrinsic_ReportUsageCounter(JSContext* cx, unsigned int argc,
JS::Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Currently usage counter is only used for reporting subclassing
// as a result we hyper-specialize the following
MOZ_ASSERT(args.length() == 2);
MOZ_ASSERT(args.get(0).isObject());
MOZ_ASSERT(args.get(1).isInt32());
HandleValue arg0 = args.get(0);
RootedObject constructor(cx, &arg0.toObject());
int32_t packedTypeAndBuiltin = args.get(1).toInt32();
int32_t type = packedTypeAndBuiltin & SUBCLASSING_TYPE_MASK;
MOZ_ASSERT(type >= SUBCLASSING_TYPE_II && type <= SUBCLASSING_TYPE_IV);
int32_t builtin = packedTypeAndBuiltin >> SUBCLASSING_BUILTIN_SHIFT;
MOZ_ASSERT(builtin > 0 && builtin < SUBCLASSING_LAST_BUILTIN);
return ReportUsageCounter(cx, constructor, builtin, type);
}
static_assert(
JSString::MAX_LENGTH <= INT32_MAX,
"StringIteratorNext in builtin/String.js assumes the stored index "

View File

@ -292,6 +292,12 @@ bool IsTupleUnchecked(JSContext* cx, const CallArgs& args);
bool intrinsic_IsTuple(JSContext* cx, unsigned argc, JS::Value* vp);
#endif
bool intrinsic_ReportUsageCounter(JSContext* cx, unsigned argc, JS::Value* vp);
// The arguments to this are defined in SelfHostingDefines.h
bool ReportUsageCounter(JSContext* cx, HandleObject constructor,
int32_t builtin, int32_t type);
} /* namespace js */
#endif /* vm_SelfHosting_h_ */

View File

@ -2628,16 +2628,21 @@ static void SetUseCounterCallback(JSObject* obj, JSUseCounter counter) {
switch (counter) {
case JSUseCounter::ASMJS:
SetUseCounter(obj, eUseCounter_custom_JS_asmjs);
break;
return;
case JSUseCounter::WASM:
SetUseCounter(obj, eUseCounter_custom_JS_wasm);
break;
return;
case JSUseCounter::WASM_LEGACY_EXCEPTIONS:
SetUseCounter(obj, eUseCounter_custom_JS_wasm_legacy_exceptions);
return;
case JSUseCounter::SUBCLASSING_ARRAY_TYPE_II:
SetUseCounter(obj,
mozilla::eUseCounter_custom_JS_subclassing_array_type_2);
return;
case JSUseCounter::COUNT:
break;
default:
MOZ_ASSERT_UNREACHABLE("Unexpected JSUseCounter id");
}
MOZ_ASSERT_UNREACHABLE("Unexpected JSUseCounter id");
}
static void GetRealmNameCallback(JSContext* cx, Realm* realm, char* buf,