Backed out 9 changesets (bug 1387894) for wpt failures at /web-animations/timing-model/animations/finishing-an-animation.html on a CLOSED TREE

Backed out changeset 998582bf083d (bug 1387894)
Backed out changeset cd58aae7d47b (bug 1387894)
Backed out changeset a51919fb2062 (bug 1387894)
Backed out changeset ef7b589d751b (bug 1387894)
Backed out changeset 75c0249b594a (bug 1387894)
Backed out changeset 4a75f2556242 (bug 1387894)
Backed out changeset 0bff9ba4237c (bug 1387894)
Backed out changeset 80040c0a275e (bug 1387894)
Backed out changeset 02814f69872d (bug 1387894)
This commit is contained in:
Daniel Varga 2019-07-30 21:05:59 +03:00
Родитель 2fd3bab4f7
Коммит c7ecfc9e12
15 изменённых файлов: 115 добавлений и 180 удалений

Просмотреть файл

@ -40,7 +40,7 @@ async function testSetCurrentTimes(walker, animations) {
const state = await players[i].getCurrentState();
is(state.playState, "paused", `Player ${i + 1} is paused`);
is(
parseInt(state.currentTime.toPrecision(4), 10),
parseInt(state.currentTime.toPrecision(6), 10),
500,
`Player ${i + 1} has the right currentTime`
);

Просмотреть файл

@ -10,10 +10,6 @@
<script>
'use strict';
function matchUnconditionalClamping(timestamp) {
return parseFloat((Math.floor(timestamp / .02) * .02).toPrecision(8), 10);
}
test(function() {
assert_equals(document.timeline, document.timeline,
'document.timeline returns the same object every time');
@ -47,7 +43,7 @@ async_test(function(t) {
// window.performance.now() because currentTime is only updated on a sample
// so we use requestAnimationFrame instead.
window.requestAnimationFrame(t.step_func(function(rafTime) {
assert_equals(document.timeline.currentTime, matchUnconditionalClamping(rafTime),
assert_equals(document.timeline.currentTime, rafTime,
'document.timeline.currentTime matches' +
' requestAnimationFrame time');
t.done();

34
dom/animation/test/mozilla/file_restyles.html Executable file → Normal file
Просмотреть файл

@ -376,40 +376,34 @@ waitForAllPaints(() => {
{ style: 'animation: rotate 100s infinite; position: relative; top: 100px;' });
parentElement.appendChild(div);
const animation = div.getAnimations()[0];
let timeAtStart = document.timeline.currentTime;
const timeAtStart = document.timeline.currentTime;
ok(!animation.isRunningOnCompositor,
'The transform animation is not running on the compositor');
let markers;
let now;
let elapsed;
while (true) {
now = document.timeline.currentTime;
elapsed = (now - timeAtStart);
markers = await observeStyling(1);
if (markers.length > 0) {
if ((now - timeAtStart) >= 200) {
// If the current time has elapsed over 200ms since the animation was
// created, it means that the animation should have already
// unthrottled in this tick, let's see what we observe in this tick's
// restyling process.
markers = await observeStyling(1);
break;
}
}
// If the current time has elapsed over 200ms since the animation was
// created, it means that the animation should have already
// unthrottled in this tick, let's see what we observe in this tick's
// restyling process.
ok(elapsed >= 200,
markers = await observeStyling(1);
is(markers.length, 0,
'Transform animation running on the element which is scrolled out ' +
'should be throttled until 200ms is elapsed. now: ' +
now + ' start time: ' + timeAtStart + ' elapsed:' + elapsed);
'should be throttled until 200ms is elapsed');
}
timeAtStart = document.timeline.currentTime;
markers = await observeStyling(1);
now = document.timeline.currentTime;
elapsed = (now - timeAtStart);
ok(markers.length == (elapsed > 200 ? 1 : 0),
is(markers.length, 1,
'Transform animation running on the element which is scrolled out ' +
'should be unthrottled after around 200ms have elapsed. now: ' +
now + ' start time: ' + timeAtStart + ' elapsed: ' + elapsed);
now + ' start time: ' + timeAtStart);
await ensureElementRemoval(parentElement);
}

Просмотреть файл

@ -94,7 +94,10 @@ DOMHighResTimeStamp Performance::Now() {
return rawTime;
}
return nsRFPService::ReduceTimePrecisionAsMSecs(rawTime,
const double maxResolutionMs = 0.020;
DOMHighResTimeStamp minimallyClamped =
floor(rawTime / maxResolutionMs) * maxResolutionMs;
return nsRFPService::ReduceTimePrecisionAsMSecs(minimallyClamped,
GetRandomTimelineSeed());
}

Просмотреть файл

@ -5529,7 +5529,7 @@
value: false
mirror: always
# Whether to spoof user locale to English (used as part of Resist Fingerprinting)
# Spoof user locale to English
- name: privacy.spoof_english
type: RelaxedAtomicUint32
value: 0
@ -5550,66 +5550,11 @@
value: @IS_NIGHTLY_BUILD@
mirror: always
# The resistFingerprinting variables are marked with 'Relaxed' memory ordering.
# We don't particurally care that threads have a percently consistent view of
# the values of these prefs. They are not expected to change often, and having
# an outdated view is not particurally harmful. They will eventually become
# consistent.
#
# The variables will, however, be read often (specifically .microseconds on
# each timer rounding) so performance is important.
- name: privacy.resistFingerprinting
type: RelaxedAtomicBool
value: false
mirror: always
# We automatically decline canvas permission requests if they are not initiated
# from user input. Just in case that breaks something, we allow the user to revert
# this behavior with this obscure pref. We do not intend to support this long term.
# If you do set it, to work around some broken website, please file a bug with
# information so we can understand why it is needed.
- name: privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts
type: bool
value: true
mirror: always
# The log level for browser console messages logged in RFPHelper.jsm
# Change to 'All' and restart to see the messages
- name: privacy.resistFingerprinting.jsmloglevel
type: String
value: "Warn"
value: false
mirror: never
# A subset of Resist Fingerprinting protections focused specifically on timers for testing
# This affects the Animation API, the performance APIs, Date.getTime, Event.timestamp,
# File.lastModified, audioContext.currentTime, canvas.captureStream.currentTime
- name: privacy.reduceTimerPrecision
type: RelaxedAtomicBool
value: true
mirror: always
# If privacy.reduceTimerPrecision is false, this pref controls whether or not to
# clamp all timers at a fixed 20 microsconds. It should always be enabled, and is
# only specified as a pref to enable an emergency disabling in the event of catastrophic
# failure
- name: privacy.reduceTimerPrecision.unconditional
type: RelaxedAtomicBool
value: true
mirror: always
# Dynamically tune the resolution of the timer reduction for both of the two above prefs
- name: privacy.resistFingerprinting.reduceTimerPrecision.microseconds
type: RelaxedAtomicUint32
value: 1000
mirror: always
# Enable jittering the clock one precision value forward
- name : privacy.resistFingerprinting.reduceTimerPrecision.jitter
type: RelaxedAtomicBool
value: true
mirror: always
# Anti-tracking permission expiration
- name: privacy.restrict3rdpartystorage.expiration
type: uint32_t
@ -5639,6 +5584,17 @@
#endif
mirror: always
# Anti-fingerprinting, disabled by default
- name: privacy.resistFingerprinting
type: RelaxedAtomicBool
value: false
mirror: always
- name: privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts
type: RelaxedAtomicBool
value: false
mirror: always
- name: privacy.storagePrincipal.enabledForTrackers
type: RelaxedAtomicBool
value: false

Просмотреть файл

@ -1149,13 +1149,29 @@ pref("privacy.popups.maxReported", 100);
#ifdef NIGHTLY_BUILD
pref("privacy.trackingprotection.origin_telemetry.enabled", true);
#endif
// First Party Isolation (double keying), disabled by default
pref("privacy.firstparty.isolate", false);
// If false, two windows in the same domain with different first party domains
// (top level URLs) can access resources through window.opener.
// This pref is effective only when "privacy.firstparty.isolate" is true.
pref("privacy.firstparty.isolate.restrict_opener_access", true);
// We automatically decline canvas permission requests if they are not initiated
// from user input. Just in case that breaks something, we allow the user to revert
// this behavior with this obscure pref. We do not intend to support this long term.
// If you do set it, to work around some broken website, please file a bug with
// information so we can understand why it is needed.
pref("privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts", true);
// The log level for browser console messages logged in RFPHelper.jsm
// Change to 'All' and restart to see the messages
pref("privacy.resistFingerprinting.jsmloglevel", "Warn");
// A subset of Resist Fingerprinting protections focused specifically on timers for testing
// This affects the Animation API, the performance APIs, Date.getTime, Event.timestamp,
// File.lastModified, audioContext.currentTime, canvas.captureStream.currentTime
pref("privacy.reduceTimerPrecision", true);
// Dynamically tune the resolution of the timer reduction for both of the two above prefs
pref("privacy.resistFingerprinting.reduceTimerPrecision.microseconds", 1000);
// Enable jittering the clock one precision value forward
pref("privacy.resistFingerprinting.reduceTimerPrecision.jitter", true);
pref("dom.event.contextmenu.enabled", true);
pref("dom.event.coalesce_mouse_move", true);

Просмотреть файл

@ -1,2 +0,0 @@
[CSSAnimation-startTime.tentative.html]
prefs: [privacy.reduceTimerPrecision.unconditional:false]

Просмотреть файл

@ -1,5 +1,4 @@
[CSSTransition-startTime.tentative.html]
prefs: [privacy.reduceTimerPrecision.unconditional:false]
disabled:
if (os == "android") and debug: https://bugzilla.mozilla.org/show_bug.cgi?id=1560466

Просмотреть файл

@ -1,3 +1,8 @@
[Event-timestamp-safe-resolution.html]
disabled:
if (os == "android") and e10s: bug 1550895 (frequently fails on geckoview)
[Event timestamp should not have a resolution better than 5 microseconds]
expected:
if not debug and not webrender and not e10s and (os == "android") and (version == "Ubuntu 16.04") and (processor == "x86") and (bits == 32): PASS
FAIL

Просмотреть файл

@ -1,18 +0,0 @@
// Firefox implements unconditional clamping of 20 usec; and for certain web-animation tests,
// we hit some test failures because the Time Precision is too small. We override these functions
// on a per-test basis for Firefox only.
if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){
window.assert_times_equal = (actual, expected, description) => {
let TIME_PRECISION = 0.02;
assert_approx_equals(actual, expected, TIME_PRECISION * 2, description);
};
window.assert_time_equals_literal = (actual, expected, description) => {
let TIME_PRECISION = 0.02;
if (Math.abs(expected) === Infinity) {
assert_equals(actual, expected, description);
} else {
assert_approx_equals(actual, expected, TIME_PRECISION, description);
}
}
}

Просмотреть файл

@ -5,7 +5,6 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/timing-override.js"></script>
<body>
<div id="log"></div>
<script>
@ -263,7 +262,7 @@ promise_test(async t => {
// calculated using the new playback rate
assert_times_equal(anim.startTime,
anim.timeline.currentTime - 25 * MS_PER_SEC);
assert_time_equals_literal(parseInt(anim.currentTime.toPrecision(5), 10), 50 * MS_PER_SEC);
assert_time_equals_literal(anim.currentTime, 50 * MS_PER_SEC);
}, 'Setting the start time of a playing animation applies a pending playback rate');
</script>

Просмотреть файл

@ -5,7 +5,6 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/timing-override.js"></script>
<body>
<div id="log"></div>
<script>

Просмотреть файл

@ -9,10 +9,6 @@
<script>
'use strict';
function matchUnconditionalClamping(timestamp) {
return parseFloat((Math.floor(timestamp / .02) * .02).toPrecision(8), 10);
}
async_test(t => {
assert_greater_than_equal(document.timeline.currentTime, 0,
'The current time is initially is positive or zero');
@ -31,7 +27,7 @@ async_test(t => {
// so we use requestAnimationFrame instead.
window.requestAnimationFrame(rafTime => {
t.step(() => {
assert_equals(document.timeline.currentTime, matchUnconditionalClamping(rafTime),
assert_equals(document.timeline.currentTime, rafTime,
'The current time matches requestAnimationFrame time');
});
t.done();

Просмотреть файл

@ -16,7 +16,6 @@
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/KeyboardEventBinding.h"
@ -49,9 +48,6 @@ static mozilla::LazyLogModule gResistFingerprintingLog(
#define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting"
#define RFP_TIMER_PREF "privacy.reduceTimerPrecision"
#define RFP_TIMER_UNCONDITIONAL_PREF \
"privacy.reduceTimerPrecision.unconditional"
#define RFP_TIMER_UNCONDITIONAL_VALUE 20
#define RFP_TIMER_VALUE_PREF \
"privacy.resistFingerprinting.reduceTimerPrecision.microseconds"
#define RFP_TIMER_VALUE_DEFAULT 1000
@ -74,8 +70,24 @@ static mozilla::LazyLogModule gResistFingerprintingLog(
NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver)
/*
* The below variables are marked with 'Relaxed' memory ordering. We don't
* particurally care that threads have a percently consistent view of the values
* of these prefs. They are not expected to change often, and having an outdated
* view is not particurally harmful. They will eventually become consistent.
*
* The variables will, however, be read often (specifically sResolutionUSec on
* each timer rounding) so performance is important.
*/
static StaticRefPtr<nsRFPService> sRFPService;
static bool sInitialized = false;
Atomic<bool, Relaxed> nsRFPService::sPrivacyResistFingerprinting;
Atomic<bool, Relaxed> nsRFPService::sPrivacyTimerPrecisionReduction;
// Note: anytime you want to use this variable, you should probably use
// TimerResolution() instead
Atomic<uint32_t, Relaxed> sResolutionUSec;
Atomic<bool, Relaxed> sJitter;
static uint32_t sVideoFramesPerSec;
static uint32_t sVideoDroppedRatio;
static uint32_t sTargetVideoRes;
@ -103,17 +115,15 @@ nsRFPService* nsRFPService::GetOrCreate() {
/* static */
double nsRFPService::TimerResolution() {
double prefValue = StaticPrefs::
privacy_resistFingerprinting_reduceTimerPrecision_microseconds();
if (nsRFPService::IsResistFingerprintingEnabled()) {
return max(100000.0, prefValue);
return max(100000.0, (double)sResolutionUSec);
}
return prefValue;
return sResolutionUSec;
}
/* static */
bool nsRFPService::IsResistFingerprintingEnabled() {
return StaticPrefs::privacy_resistFingerprinting();
return sPrivacyResistFingerprinting;
}
/* static */
@ -122,8 +132,7 @@ bool nsRFPService::IsTimerPrecisionReductionEnabled(TimerPrecisionType aType) {
return IsResistFingerprintingEnabled();
}
return (StaticPrefs::privacy_reduceTimerPrecision() ||
IsResistFingerprintingEnabled()) &&
return (sPrivacyTimerPrecisionReduction || IsResistFingerprintingEnabled()) &&
TimerResolution() > 0;
}
@ -477,23 +486,7 @@ double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale,
double aResolutionUSec,
int64_t aContextMixin,
TimerPrecisionType aType) {
// This boolean will serve as a flag indicating we are clamping the time
// unconditionally. We do this when timer reduction preference is off; but we
// still want to apply 20us clamping to al timestamps to avoid leaking
// nano-second precision.
bool unconditionalClamping = false;
if (!IsTimerPrecisionReductionEnabled(aType)) {
if (!StaticPrefs::privacy_reduceTimerPrecision_unconditional()) {
return aTime;
} else {
unconditionalClamping = true;
aResolutionUSec = RFP_TIMER_UNCONDITIONAL_VALUE; // 20 microseconds
aContextMixin = 0; // Just clarifies our logging statement at the end,
// otherwise unused
}
}
if (aResolutionUSec <= 0) {
if (!IsTimerPrecisionReductionEnabled(aType) || aResolutionUSec <= 0) {
return aTime;
}
@ -513,14 +506,13 @@ double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale,
// given a relative timestamp with a mixin of 0 which is incorrect. Anyone
// running a debug build _probably_ has an accurate clock, and if they don't,
// they'll hopefully find this message and understand why things are crashing.
const long long kFeb282008 = 1204233985000;
if (!unconditionalClamping && aContextMixin == 0 &&
aType == TimerPrecisionType::All && timeAsInt < kFeb282008) {
MOZ_LOG(
gResistFingerprintingLog, LogLevel::Error,
("About to assert. aTime=%lli<%lli aContextMixin=%" PRId64 " aType=%s",
timeAsInt, kFeb282008, aContextMixin,
(aType == TimerPrecisionType::RFPOnly ? "RFPOnly" : "All")));
if (aContextMixin == 0 && aType == TimerPrecisionType::All &&
timeAsInt < 1204233985000) {
MOZ_LOG(gResistFingerprintingLog, LogLevel::Error,
("About to assert. aTime=%lli<1204233985000 aContextMixin=%" PRId64
" aType=%s",
timeAsInt, aContextMixin,
(aType == TimerPrecisionType::RFPOnly ? "RFPOnly" : "All")));
MOZ_ASSERT(
false,
"ReduceTimePrecisionImpl was given a relative time "
@ -546,8 +538,7 @@ double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale,
floor(double(timeAsInt) / resolutionAsInt) * resolutionAsInt;
long long midpoint = 0, clampedAndJittered = clamped;
if (!unconditionalClamping &&
StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter()) {
if (sJitter) {
if (!NS_FAILED(RandomMidpoint(clamped, resolutionAsInt, aContextMixin,
&midpoint)) &&
timeAsInt >= clamped + midpoint) {
@ -558,19 +549,18 @@ double nsRFPService::ReduceTimePrecisionImpl(double aTime, TimeScale aTimeScale,
// Cast it back to a double and reduce it to the correct units.
double ret = double(clampedAndJittered) / (1000000.0 / aTimeScale);
MOZ_LOG(
gResistFingerprintingLog, LogLevel::Verbose,
("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding %s with (%lli, "
"Originally %.*f), "
"Intermediate: (%lli), Clamped: (%lli) Jitter: (%i Context: %" PRId64
" Midpoint: %lli) "
"Final: (%lli Converted: %.*f)",
DBL_DIG - 1, aTime, DBL_DIG - 1, timeScaled, timeAsInt,
(unconditionalClamping ? "unconditionally" : "normally"),
resolutionAsInt, DBL_DIG - 1, aResolutionUSec,
(long long)floor(double(timeAsInt) / resolutionAsInt), clamped,
StaticPrefs::privacy_resistFingerprinting_reduceTimerPrecision_jitter(),
aContextMixin, midpoint, clampedAndJittered, DBL_DIG - 1, ret));
bool tmp_jitter = sJitter;
MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding with (%lli, "
"Originally %.*f), "
"Intermediate: (%lli), Clamped: (%lli) Jitter: (%i Context: %" PRId64
" Midpoint: %lli) "
"Final: (%lli Converted: %.*f)",
DBL_DIG - 1, aTime, DBL_DIG - 1, timeScaled, timeAsInt,
resolutionAsInt, DBL_DIG - 1, aResolutionUSec,
(long long)floor(double(timeAsInt) / resolutionAsInt), clamped,
tmp_jitter, aContextMixin, midpoint, clampedAndJittered, DBL_DIG - 1,
ret));
return ret;
}
@ -714,9 +704,8 @@ void nsRFPService::GetSpoofedUserAgent(nsACString& userAgent,
}
static const char* gCallbackPrefs[] = {
RESIST_FINGERPRINTING_PREF, RFP_TIMER_PREF,
RFP_TIMER_UNCONDITIONAL_PREF, RFP_TIMER_VALUE_PREF,
RFP_JITTER_VALUE_PREF, nullptr,
RESIST_FINGERPRINTING_PREF, RFP_TIMER_PREF, RFP_TIMER_VALUE_PREF,
RFP_JITTER_VALUE_PREF, nullptr,
};
nsresult nsRFPService::Init() {
@ -738,6 +727,13 @@ nsresult nsRFPService::Init() {
Preferences::RegisterCallbacks(PREF_CHANGE_METHOD(nsRFPService::PrefChanged),
gCallbackPrefs, this);
Preferences::AddAtomicBoolVarCache(&sPrivacyTimerPrecisionReduction,
RFP_TIMER_PREF, true);
Preferences::AddAtomicUintVarCache(&sResolutionUSec, RFP_TIMER_VALUE_PREF,
RFP_TIMER_VALUE_DEFAULT);
Preferences::AddAtomicBoolVarCache(&sJitter, RFP_JITTER_VALUE_PREF,
RFP_JITTER_VALUE_DEFAULT);
Preferences::AddUintVarCache(&sVideoFramesPerSec,
RFP_SPOOFED_FRAMES_PER_SEC_PREF,
RFP_SPOOFED_FRAMES_PER_SEC_DEFAULT);
@ -769,16 +765,8 @@ nsresult nsRFPService::Init() {
void nsRFPService::UpdateTimers() {
MOZ_ASSERT(NS_IsMainThread());
if (StaticPrefs::privacy_resistFingerprinting() ||
StaticPrefs::privacy_reduceTimerPrecision()) {
JS::SetTimeResolutionUsec(
TimerResolution(),
StaticPrefs::
privacy_resistFingerprinting_reduceTimerPrecision_jitter());
JS::SetReduceMicrosecondTimePrecisionCallback(
nsRFPService::ReduceTimePrecisionAsUSecsWrapper);
} else if (StaticPrefs::privacy_reduceTimerPrecision_unconditional()) {
JS::SetTimeResolutionUsec(RFP_TIMER_UNCONDITIONAL_VALUE, false);
if (sPrivacyResistFingerprinting || sPrivacyTimerPrecisionReduction) {
JS::SetTimeResolutionUsec(TimerResolution(), sJitter);
JS::SetReduceMicrosecondTimePrecisionCallback(
nsRFPService::ReduceTimePrecisionAsUSecsWrapper);
} else if (sInitialized) {
@ -790,10 +778,12 @@ void nsRFPService::UpdateTimers() {
// timing-related
void nsRFPService::UpdateRFPPref() {
MOZ_ASSERT(NS_IsMainThread());
sPrivacyResistFingerprinting =
Preferences::GetBool(RESIST_FINGERPRINTING_PREF);
UpdateTimers();
if (StaticPrefs::privacy_resistFingerprinting()) {
if (sPrivacyResistFingerprinting) {
PR_SetEnv("TZ=UTC");
} else if (sInitialized) {
// We will not touch the TZ value if 'privacy.resistFingerprinting' is false
@ -1066,7 +1056,6 @@ void nsRFPService::PrefChanged(const char* aPref) {
nsDependentCString pref(aPref);
if (pref.EqualsLiteral(RFP_TIMER_PREF) ||
pref.EqualsLiteral(RFP_TIMER_UNCONDITIONAL_PREF) ||
pref.EqualsLiteral(RFP_TIMER_VALUE_PREF) ||
pref.EqualsLiteral(RFP_JITTER_VALUE_PREF)) {
UpdateTimers();

3
toolkit/components/resistfingerprinting/nsRFPService.h Executable file → Normal file
Просмотреть файл

@ -255,6 +255,9 @@ class nsRFPService final : public nsIObserver {
const WidgetKeyboardEvent* aKeyboardEvent,
SpoofingKeyboardCode& aOut);
static Atomic<bool, Relaxed> sPrivacyResistFingerprinting;
static Atomic<bool, Relaxed> sPrivacyTimerPrecisionReduction;
static nsDataHashtable<KeyboardHashKey, const SpoofingKeyboardCode*>*
sSpoofingKeyboardCodes;