Compare commits

..
5 Commits
26 changed files with 436 additions and 866 deletions

No files matched your search

+2 -15
View File
@@ -13,7 +13,6 @@
#include "jsgc.h"
#include "gc/Heap.h"
#include "gc/IdleGC.h"
#include "gc/Nursery.h"
#include "gc/Statistics.h"
#include "gc/StoreBuffer.h"
@@ -651,13 +650,6 @@ class GCRuntime
void onOutOfMallocMemory();
void onOutOfMallocMemory(const AutoLockGC& lock);
/* Idle-time GC notifications. */
void notifyJSExecutionStart();
void notifyJSExecutionEnd();
IdleGCManager& idleGCMgr() {
return idleGC;
}
size_t maxMallocBytesAllocated() { return maxMallocBytes; }
uint64_t nextCellUniqueId() {
@@ -974,8 +966,6 @@ class GCRuntime
void sweepZones(FreeOp* fop, bool lastGC);
void decommitAllWithoutUnlocking(const AutoLockGC& lock);
void startDecommit();
bool sweepBackgroundFinalizePhaseInParallel(ZoneList& zones, const FinalizePhase& phase,
Arena** emptyArenas);
void queueZonesForBackgroundSweep(ZoneList& zones);
void sweepBackgroundThings(ZoneList& zones, LifoAlloc& freeBlocks);
void assertBackgroundSweepingFinished();
@@ -987,9 +977,9 @@ class GCRuntime
void sweepTypesAfterCompacting(Zone* zone);
void sweepZoneAfterCompacting(Zone* zone);
[[nodiscard]] bool relocateArenas(Zone* zone, JS::gcreason::Reason reason,
Arena*& relocatedListOut, SliceBudget& sliceBudget);
Arena*& relocatedListOut, SliceBudget& sliceBudget);
void updateTypeDescrObjects(MovingTracer* trc, Zone* zone);
void updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds);
void updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds, size_t bgTaskCount);
void updateAllCellPointers(MovingTracer* trc, Zone* zone);
void updatePointersToRelocatedCells(Zone* zone, AutoLockForExclusiveAccess& lock);
void protectAndHoldArenas(Arena* arenaList);
@@ -1029,9 +1019,6 @@ class GCRuntime
GCSchedulingTunables tunables;
GCSchedulingState schedulingState;
/* Idle-time garbage collection manager. */
IdleGCManager idleGC;
MemProfiler mMemProfiler;
private:
-262
View File
@@ -1,262 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* IDLE-TIME GARBAGE COLLECTION SYSTEM
*
* Overview
* --------
* This system implements idle-time-only garbage collection, deferring GC work
* to periods when the JavaScript engine is not actively processing code.
* This improves perceived responsiveness by avoiding GC pauses during critical
* execution windows.
*
* Architecture
* -----------
*
* The system consists of several key components:
*
* 1. IdleGCManager (gc/IdleGC.h, gc/IdleGC.cpp)
* - Tracks when the JS engine is executing vs. idle
* - Maintains timestamp of last execution activity
* - Checks if sufficient idle time has passed
* - Configurable idle threshold (default: 100ms)
* - Can determine which GC reasons should bypass idle checks
*
* 2. GCRuntime Integration (gc/GCRuntime.h)
* - Contains an IdleGCManager instance
* - Provides public methods: notifyJSExecutionStart/End()
* - Modified checkIfGCAllowedInCurrentState() to check idle status
*
* 3. Activity Tracking (vm/Runtime.cpp)
* - triggerActivityCallback() now notifies IdleGCManager
* - Called when JS enters/exits request (execution boundary)
* - Updated in jsapi.cpp's StartRequest/StopRequest functions
*
* 4. Public API (jsapi.h, jsapi.cpp)
* - JS_SetIdleGCEnabled() / JS_IsIdleGCEnabled()
* - JS_SetIdleGCThreshold() / JS_GetIdleGCThreshold()
* - JS_GetIdleTimeSinceLastExecution()
* - Allows embedders to configure idle GC behavior
*
* Behavior
* --------
*
* Normal GC Trigger (Idle GC Enabled):
*
* 1. JS code executes → notifyJSExecutionStart() called
* 2. JS code finishes → notifyJSExecutionEnd() called, timestamp recorded
* 3. GC needed → checkIfGCAllowedInCurrentState() checks idle status
* 4a. If idle >= threshold → GC proceeds normally
* 4b. If still executing/idle < threshold → GC is deferred
* 5. Once idle threshold is met, next GC request proceeds
*
* Critical GC Triggers (Always Proceed):
*
* The following GC reasons bypass idle checking:
* - OUT_OF_MEMORY: Memory pressure conditions
* - ALLOC_TRIGGER: Allocation threshold exceeded
* - MALLOC_PRESSURE: Malloc pressure from OS
* - EAGER_ALLOC_TRIGGER: Eager allocation trigger
* - API: Explicit JS API calls
* - DETERMINISTIC: Deterministic tests
* - EVICT_NURSERY: Nursery eviction
* - SHUTDOWN_CC, DESTROY_RUNTIME, LAST_DITCH: Shutdown GCs
* - DESTROY_ZONE, COMPARTMENT_REVOKED: Zone/compartment destruction
*
* Configuration
* -------------
*
* Idle GC Enabled (default: true):
* - Enables idle-time-only GC mode
* - Can be toggled dynamically via JS_SetIdleGCEnabled()
*
* Idle Threshold (default: 100ms):
* - Minimum idle time before GC is permitted
* - Configurable via JS_SetIdleGCThreshold(ms)
* - Typical values: 50-200ms depending on application
*
* Integration Examples
* --------------------
*
* Browser Integration:
*
* // When browser loads configuration
* JS_SetIdleGCEnabled(cx, true);
* JS_SetIdleGCThreshold(cx, 100); // 100ms idle threshold
*
* // Monitor idle GC effectiveness (optional)
* uint64_t idleTime = JS_GetIdleTimeSinceLastExecution(cx);
* if (idleTime > JS_GetIdleGCThreshold(cx)) {
* // System is idle, GC would be allowed if triggered
* }
*
* Disabling for Specific Scenarios:
*
* // During initialization when nothing is "idle" yet
* JS_SetIdleGCEnabled(cx, false);
* // ... do initial setup ...
* JS_SetIdleGCEnabled(cx, true); // Re-enable for normal operation
*
* Performance Considerations
* --------------------------
*
* Benefits:
* - Reduced jank during active JS execution
* - GC pauses moved to idle periods where users won't notice
* - Especially effective for interactive applications
* - Improves Time-to-Interactive and First Input Delay metrics
*
* Tradeoffs:
* - May accumulate more garbage before collection
* - Requires predictable idle periods (not suitable for all workloads)
* - Critical memory pressure GCs still proceed immediately
*
* Tuning:
* - Lower threshold (50ms) = more frequent GC, less memory overhead
* - Higher threshold (200ms) = less GC overhead, more memory usage
* - Optimal value depends on application's execution pattern
*
* Testing
* -------
*
* Unit Tests:
* // Test idle detection
* JS_SetIdleGCThreshold(cx, 100);
* // Simulate JS execution
* cx->runtime()->gc.notifyJSExecutionStart();
* // ... wait 50ms ...
* MOZ_ASSERT(!cx->runtime()->gc.idleGCMgr().isIdleEnough());
* cx->runtime()->gc.notifyJSExecutionEnd();
* // ... wait 150ms ...
* MOZ_ASSERT(cx->runtime()->gc.idleGCMgr().isIdleEnough());
*
* Integration Tests:
* - Verify GC is deferred during active execution
* - Verify GC proceeds after idle period
* - Verify critical GC reasons bypass idle check
* - Measure latency improvements
*
* Implementation Notes
* --------------------
*
* Thread Safety:
* - IdleGCManager uses mozilla::Atomic for thread-safe state
* - TimeStamp operations are atomic
* - No additional locking needed beyond existing GC locks
*
* Compatibility:
* - Works with both incremental and non-incremental GC
* - Compatible with generational GC
* - Works with zone GC and full GC
* - Respects existing GC suppression mechanisms
*
* Future Enhancements
* -------------------
*
* Potential improvements:
* - Adaptive idle threshold based on historical GC times
* - Per-zone idle configuration
* - Integration with browser rendering idle callback API
* - Metrics/telemetry for idle GC effectiveness
* - Machine learning-based prediction of idle periods
* - Cooperative GC scheduling with other subsystems
*
*/
#include "gc/IdleGC.h"
#include "jsapi.h"
namespace js {
namespace gc {
IdleGCManager::IdleGCManager()
: lastExecutionTime_(mozilla::TimeStamp::Now()),
idleGCEnabled_(true),
idleThresholdMs_(100), // 100ms default idle threshold
isExecuting_(false)
{
}
void
IdleGCManager::notifyJSExecutionStart()
{
isExecuting_ = true;
}
void
IdleGCManager::notifyJSExecutionEnd()
{
isExecuting_ = false;
lastExecutionTime_ = mozilla::TimeStamp::Now();
}
bool
IdleGCManager::isIdleEnough() const
{
if (!idleGCEnabled_) {
return true; // If disabled, always consider idle
}
if (isExecuting_) {
return false; // Still executing, not idle
}
uint64_t idleTime = idleTimeSinceLastExecution();
return idleTime >= idleThresholdMs_;
}
uint64_t
IdleGCManager::idleTimeSinceLastExecution() const
{
mozilla::TimeStamp now = mozilla::TimeStamp::Now();
mozilla::TimeDuration idle = now - lastExecutionTime_;
return idle.ToMilliseconds();
}
bool
IdleGCManager::shouldBypassIdleCheck(JS::gcreason::Reason reason)
{
// These reasons indicate urgent GC needs that should bypass idle checking
switch (reason) {
// Allocation and memory pressure conditions.
case JS::gcreason::ALLOC_TRIGGER:
case JS::gcreason::EAGER_ALLOC_TRIGGER:
case JS::gcreason::TOO_MUCH_MALLOC:
case JS::gcreason::MEM_PRESSURE:
case JS::gcreason::LAST_DITCH:
// Nursery/store-buffer pressure.
case JS::gcreason::OUT_OF_NURSERY:
case JS::gcreason::EVICT_NURSERY:
case JS::gcreason::FULL_STORE_BUFFER:
case JS::gcreason::SHARED_MEMORY_LIMIT:
// Explicit API calls
case JS::gcreason::API:
case JS::gcreason::ABORT_GC:
// Shutdown and finalization
case JS::gcreason::SHUTDOWN_CC:
case JS::gcreason::DESTROY_RUNTIME:
case JS::gcreason::NSJSCONTEXT_DESTROY:
case JS::gcreason::XPCONNECT_SHUTDOWN:
return true;
default:
return false;
}
}
void
IdleGCManager::reset()
{
lastExecutionTime_ = mozilla::TimeStamp::Now();
isExecuting_ = false;
}
} // namespace gc
} // namespace js
-118
View File
@@ -1,118 +0,0 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef gc_IdleGC_h
#define gc_IdleGC_h
#include <stdint.h>
#include "mozilla/Atomics.h"
#include "mozilla/TimeStamp.h"
#include "js/GCAPI.h"
namespace js {
namespace gc {
/*
* Idle-Time Garbage Collection System
* ====================================
*
* This system defers garbage collection to occur only during periods when
* the browser is not actively processing JavaScript. This helps maintain
* responsiveness by avoiding GC pauses during critical execution windows.
*
* When JavaScript execution is active, GC triggers are deferred. Once the
* JS engine has been idle for a configurable threshold period, pending GC
* work is performed immediately or incrementally as appropriate.
*
* Key characteristics:
* - Tracks JavaScript activity via hooks in the execution engine
* - Configurable idle time threshold (default: 100ms)
* - Can be disabled per-zone or globally
* - Works with both incremental and non-incremental GC modes
* - Respects critical GC reasons that override idle checking
*/
class IdleGCManager
{
public:
// Initialize the idle GC manager
IdleGCManager();
/*
* Called when JavaScript execution begins. Marks the engine as active.
*/
void notifyJSExecutionStart();
/*
* Called when JavaScript execution ends. Records the end time for
* idle detection purposes.
*/
void notifyJSExecutionEnd();
/*
* Check if the system has been idle for long enough to permit GC.
* Returns true if sufficient idle time has passed since last JS execution.
*/
bool isIdleEnough() const;
/*
* Get the amount of idle time since the last JS execution.
* Returns time in milliseconds.
*/
uint64_t idleTimeSinceLastExecution() const;
/*
* Set the idle threshold - minimum idle time before GC is permitted.
* Time is in milliseconds. Default is 100ms.
*/
void setIdleThresholdMs(uint64_t thresholdMs) {
idleThresholdMs_ = thresholdMs;
}
uint64_t idleThresholdMs() const {
return idleThresholdMs_;
}
/*
* Enable or disable idle-time-only GC mode.
*/
void setIdleGCEnabled(bool enabled) {
idleGCEnabled_ = enabled;
}
bool isIdleGCEnabled() const {
return idleGCEnabled_;
}
/*
* Check if a GC reason should bypass idle checking.
* Critical reasons (OOM-like pressure, nursery pressure, explicit requests)
* always proceed.
*/
static bool shouldBypassIdleCheck(JS::gcreason::Reason reason);
/*
* Reset idle tracking state (used during GC or at shutdown).
*/
void reset();
private:
// Timestamp of the last JavaScript execution activity
mozilla::TimeStamp lastExecutionTime_;
// Whether idle-time-only GC mode is enabled
mozilla::Atomic<bool, mozilla::ReleaseAcquire> idleGCEnabled_;
// Minimum idle time (in milliseconds) before GC is permitted
mozilla::Atomic<uint64_t, mozilla::ReleaseAcquire> idleThresholdMs_;
// Whether the JS engine is currently executing
mozilla::Atomic<bool, mozilla::ReleaseAcquire> isExecuting_;
};
} // namespace gc
} // namespace js
#endif // gc_IdleGC_h
-30
View File
@@ -1478,36 +1478,6 @@ JS_SetGCParametersBasedOnAvailableMemory(JSContext* cx, uint32_t availMem)
JS_SetGCParameter(cx, config[i].key, config[i].value);
}
JS_PUBLIC_API(void)
JS_SetIdleGCEnabled(JSContext* cx, bool enabled)
{
cx->gc.idleGCMgr().setIdleGCEnabled(enabled);
}
JS_PUBLIC_API(bool)
JS_IsIdleGCEnabled(JSContext* cx)
{
return cx->gc.idleGCMgr().isIdleGCEnabled();
}
JS_PUBLIC_API(void)
JS_SetIdleGCThreshold(JSContext* cx, uint64_t milliseconds)
{
cx->gc.idleGCMgr().setIdleThresholdMs(milliseconds);
}
JS_PUBLIC_API(uint64_t)
JS_GetIdleGCThreshold(JSContext* cx)
{
return cx->gc.idleGCMgr().idleThresholdMs();
}
JS_PUBLIC_API(uint64_t)
JS_GetIdleTimeSinceLastExecution(JSContext* cx)
{
return cx->gc.idleGCMgr().idleTimeSinceLastExecution();
}
JS_PUBLIC_API(JSString*)
JS_NewExternalString(JSContext* cx, const char16_t* chars, size_t length,
+7 -41
View File
@@ -1719,40 +1719,6 @@ JS_GetGCParameter(JSContext* cx, JSGCParamKey key);
extern JS_PUBLIC_API(void)
JS_SetGCParametersBasedOnAvailableMemory(JSContext* cx, uint32_t availMem);
/*
* Idle-time garbage collection control.
* These functions allow control over when GC occurs - specifically, whether
* GC should only run when the browser/application is not doing active JS work.
*/
/**
* Enable or disable idle-time-only GC mode.
* When enabled, GC is deferred until the JS engine has been idle for the
* configured threshold period (default: 100ms).
*/
extern JS_PUBLIC_API(void)
JS_SetIdleGCEnabled(JSContext* cx, bool enabled);
extern JS_PUBLIC_API(bool)
JS_IsIdleGCEnabled(JSContext* cx);
/**
* Set the minimum idle time (in milliseconds) before GC is permitted.
* Use this to configure how long the JS engine must be inactive before
* pending GC work can proceed.
*/
extern JS_PUBLIC_API(void)
JS_SetIdleGCThreshold(JSContext* cx, uint64_t milliseconds);
extern JS_PUBLIC_API(uint64_t)
JS_GetIdleGCThreshold(JSContext* cx);
/**
* Get the current idle time since the last JS execution.
*/
extern JS_PUBLIC_API(uint64_t)
JS_GetIdleTimeSinceLastExecution(JSContext* cx);
/**
* Create a new JSString whose chars member refers to external memory, i.e.,
* memory requiring application-specific finalization.
@@ -4904,15 +4870,15 @@ class JS_PUBLIC_API(JSErrorNotes)
js::UniquePtr<JSErrorNotes> copy(JSContext* cx);
class iterator
{
{
private:
js::UniquePtr<Note>* note_;
public:
using iterator_category = std::input_iterator_tag;
using value_type = js::UniquePtr<Note>;
using difference_type = ptrdiff_t;
using pointer = value_type*;
using reference = value_type&;
public:
using iterator_category = std::input_iterator_tag;
using value_type = js::UniquePtr<Note>;
using difference_type = ptrdiff_t;
using pointer = value_type*;
using reference = value_type&;
explicit iterator(js::UniquePtr<Note>* note = nullptr) : note_(note)
{}
+56 -272
View File
@@ -214,7 +214,6 @@
#include "gc/FindSCCs.h"
#include "gc/GCInternals.h"
#include "gc/GCTrace.h"
#include "gc/IdleGC.h"
#include "gc/Marking.h"
#include "gc/Memory.h"
#include "gc/Policy.h"
@@ -1571,7 +1570,7 @@ ArenaLists::prepareForIncrementalGC()
{
purge();
for (auto i : AllAllocKinds()) {
arenaLists[i].moveCursorToEnd();
arenaLists[i].moveCursorToEnd();
}
}
@@ -2118,7 +2117,7 @@ ArenasToUpdate::next(AutoLockHelperThreadState& lock)
// Find the next arena to update.
//
// This iterates through the GC thing kinds filtered by shouldProcessKind(),
// and then through the arenas of that kind. All state is held in the
// and then through thea arenas of that kind. All state is held in the
// object and we just return when we find an arena.
for (; kind < AllocKind::LIMIT; kind = nextAllocKind(kind)) {
@@ -2208,12 +2207,21 @@ UpdatePointersTask::run()
} // namespace gc
} // namespace js
static const size_t MinCellUpdateBackgroundTasks = 1;
static const size_t MinCellUpdateBackgroundTasks = 2;
static const size_t MaxCellUpdateBackgroundTasks = 8;
static bool
CanUpdateKindInBackground(AllocKind kind)
static size_t
CellUpdateBackgroundTaskCount()
{
if (!CanUseExtraThreads())
return 0;
size_t targetTaskCount = HelperThreadState().cpuCount / 2;
return Min(Max(targetTaskCount, MinCellUpdateBackgroundTasks), MaxCellUpdateBackgroundTasks);
}
static bool
CanUpdateKindInBackground(AllocKind kind) {
// We try to update as many GC things in parallel as we can, but there are
// kinds for which this might not be safe:
// - we assume JSObjects that are foreground finalized are not safe to
@@ -2225,34 +2233,6 @@ CanUpdateKindInBackground(AllocKind kind)
return true;
}
static size_t
CountBackgroundUpdateArenas(Zone* zone, AllocKinds kinds)
{
size_t arenaCount = 0;
for (AllocKind kind : kinds) {
MOZ_ASSERT(CanUpdateKindInBackground(kind));
for (Arena* arena = zone->arenas.getFirstArena(kind); arena; arena = arena->next)
arenaCount++;
}
return arenaCount;
}
static size_t
CellUpdateBackgroundTaskCount(Zone* zone, AllocKinds kinds)
{
if (!CanUseExtraThreads() || kinds.isEmpty())
return 0;
size_t arenaCount = CountBackgroundUpdateArenas(zone, kinds);
if (arenaCount < UpdatePointersTask::MaxArenasToProcess * 2)
return 0;
size_t targetTaskCount = HelperThreadState().cpuCount / 2;
size_t workTaskCount = arenaCount / UpdatePointersTask::MaxArenasToProcess;
targetTaskCount = Min(targetTaskCount, workTaskCount);
return Min(Max(targetTaskCount, MinCellUpdateBackgroundTasks), MaxCellUpdateBackgroundTasks);
}
static AllocKinds
ForegroundUpdateKinds(AllocKinds kinds)
{
@@ -2273,15 +2253,10 @@ GCRuntime::updateTypeDescrObjects(MovingTracer* trc, Zone* zone)
}
void
GCRuntime::updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds)
GCRuntime::updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds, size_t bgTaskCount)
{
MOZ_ASSERT(trc);
AllocKinds fgKinds = ForegroundUpdateKinds(kinds);
AllocKinds fgKinds = bgTaskCount == 0 ? kinds : ForegroundUpdateKinds(kinds);
AllocKinds bgKinds = kinds - fgKinds;
size_t bgTaskCount = CellUpdateBackgroundTaskCount(zone, bgKinds);
if (bgTaskCount == 0)
fgKinds = kinds;
ArenasToUpdate fgArenas(zone, fgKinds);
ArenasToUpdate bgArenas(zone, bgKinds);
@@ -2376,13 +2351,15 @@ GCRuntime::updateAllCellPointers(MovingTracer* trc, Zone* zone)
{
AutoDisableProxyCheck noProxyCheck(rt); // These checks assert when run in parallel.
updateCellPointers(trc, zone, UpdatePhaseMisc);
size_t bgTaskCount = CellUpdateBackgroundTaskCount();
updateCellPointers(trc, zone, UpdatePhaseMisc, bgTaskCount);
// Update TypeDescrs before all other objects as typed objects access these
// objects when we trace them.
updateTypeDescrObjects(trc, zone);
updateCellPointers(trc, zone, UpdatePhaseObjects);
updateCellPointers(trc, zone, UpdatePhaseObjects, bgTaskCount);
}
/*
@@ -2688,17 +2665,6 @@ ArenaLists::backgroundFinalize(FreeOp* fop, Arena* listHead, Arena** empty)
lists->backgroundFinalizeState[thingKind] = BFS_DONE;
}
void
ArenaLists::backgroundFinalizePhase(FreeOp* fop, const FinalizePhase& phase, Arena** empty)
{
for (auto kind : phase.kinds) {
Arena* arenas = arenaListsToSweep[kind];
MOZ_RELEASE_ASSERT(uintptr_t(arenas) != uintptr_t(-1));
if (arenas)
backgroundFinalize(fop, arenas, empty);
}
}
void
ArenaLists::queueForegroundObjectsForSweep(FreeOp* fop)
{
@@ -3037,138 +3003,6 @@ js::gc::BackgroundDecommitTask::run()
}
}
class BackgroundFinalizeTask : public GCParallelTaskHelper<BackgroundFinalizeTask>
{
Zone* zone_;
const FinalizePhase* phase_;
Arena* emptyArenas_;
BackgroundFinalizeTask(const BackgroundFinalizeTask&) = delete;
public:
BackgroundFinalizeTask(Zone* zone, const FinalizePhase* phase)
: zone_(zone),
phase_(phase),
emptyArenas_(nullptr)
{}
BackgroundFinalizeTask(BackgroundFinalizeTask&& other)
: GCParallelTaskHelper(mozilla::Move(other)),
zone_(other.zone_),
phase_(other.phase_),
emptyArenas_(other.emptyArenas_)
{
other.emptyArenas_ = nullptr;
}
void run() {
AutoSetThreadIsSweeping threadIsSweeping;
finalize();
}
void runAlreadySweeping() {
#ifdef DEBUG
MOZ_ASSERT(CurrentThreadIsGCSweeping());
#endif
finalize();
}
private:
void finalize() {
FreeOp fop(nullptr);
zone_->arenas.backgroundFinalizePhase(&fop, *phase_, &emptyArenas_);
}
public:
Arena* takeEmptyArenas() {
Arena* empty = emptyArenas_;
emptyArenas_ = nullptr;
return empty;
}
};
using BackgroundFinalizeTaskVector =
Vector<BackgroundFinalizeTask, 0, SystemAllocPolicy>;
static size_t
IdleHelperThreadCount(const AutoLockHelperThreadState&)
{
if (!HelperThreadState().threads)
return 0;
size_t idle = 0;
for (const auto& thread : *HelperThreadState().threads) {
if (thread.idle())
idle++;
}
return idle;
}
static void
PrependArenaList(Arena** head, Arena* arenas)
{
if (!arenas)
return;
Arena* tail = arenas;
while (tail->next)
tail = tail->next;
tail->next = *head;
*head = arenas;
}
bool
GCRuntime::sweepBackgroundFinalizePhaseInParallel(ZoneList& zones, const FinalizePhase& phase,
Arena** emptyArenas)
{
if (!CanUseExtraThreads())
return false;
size_t zoneCount = 0;
for (Zone* zone = zones.front(); zone; zone = zone->nextZone())
zoneCount++;
if (zoneCount < 2)
return false;
BackgroundFinalizeTaskVector tasks;
if (!tasks.reserve(zoneCount))
return false;
for (Zone* zone = zones.front(); zone; zone = zone->nextZone())
tasks.infallibleEmplaceBack(zone, &phase);
size_t tasksStarted = 0;
{
AutoLockHelperThreadState helperLock;
// sweepBackgroundThings() itself runs as a GC helper task. Do not queue
// nested GC parallel tasks unless at least one other helper is idle.
if (IdleHelperThreadCount(helperLock) == 0)
return false;
for (; tasksStarted < tasks.length(); tasksStarted++) {
if (!tasks[tasksStarted].startWithLockHeld(helperLock))
break;
}
{
AutoUnlockHelperThreadState unlock(helperLock);
for (size_t i = tasksStarted; i < tasks.length(); i++)
tasks[i].runAlreadySweeping();
}
for (size_t i = 0; i < tasksStarted; i++)
tasks[i].joinWithLockHeld(helperLock);
}
for (auto& task : tasks)
PrependArenaList(emptyArenas, task.takeEmptyArenas());
return true;
}
void
GCRuntime::sweepBackgroundThings(ZoneList& zones, LifoAlloc& freeBlocks)
{
@@ -3181,12 +3015,14 @@ GCRuntime::sweepBackgroundThings(ZoneList& zones, LifoAlloc& freeBlocks)
Arena* emptyArenas = nullptr;
FreeOp fop(nullptr);
for (unsigned phase = 0 ; phase < ArrayLength(BackgroundFinalizePhases) ; ++phase) {
const FinalizePhase& finalizePhase = BackgroundFinalizePhases[phase];
if (sweepBackgroundFinalizePhaseInParallel(zones, finalizePhase, &emptyArenas))
continue;
for (Zone* zone = zones.front(); zone; zone = zone->nextZone())
zone->arenas.backgroundFinalizePhase(&fop, finalizePhase, &emptyArenas);
for (Zone* zone = zones.front(); zone; zone = zone->nextZone()) {
for (auto kind : BackgroundFinalizePhases[phase].kinds) {
Arena* arenas = zone->arenas.arenaListsToSweep[kind];
MOZ_RELEASE_ASSERT(uintptr_t(arenas) != uintptr_t(-1));
if (arenas)
ArenaLists::backgroundFinalize(&fop, arenas, &emptyArenas);
}
}
}
AutoLockGC lock(rt);
@@ -3745,14 +3581,14 @@ ShouldCollectZone(Zone* zone, JS::gcreason::Reason reason)
// Normally we collect all scheduled zones.
if (reason != JS::gcreason::COMPARTMENT_REVIVED)
return zone->isGCScheduled();
// If we are repeating a GC because we noticed dead compartments haven't
// been collected, then only collect zones containing those compartments.
for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) {
if (comp->scheduledForDestruction)
return true;
}
return false;
}
@@ -3818,7 +3654,7 @@ GCRuntime::beginMarkPhase(JS::gcreason::Reason reason, AutoLockForExclusiveAcces
* on. If the value of keepAtoms() changes between GC slices, then we'll
* cancel the incremental GC. See IsIncrementalGCSafe.
*/
if (isFull && !rt->keepAtoms()) {
Zone* atomsZone = rt->atomsCompartment(lock)->zone();
if (atomsZone->isGCScheduled()) {
@@ -3834,7 +3670,7 @@ GCRuntime::beginMarkPhase(JS::gcreason::Reason reason, AutoLockForExclusiveAcces
/*
* Ensure that after the start of a collection we don't allocate into any
* existing arenas, as this can cause unreachable things to be marked.
* existing arenas, as this can cause unreachable things to be marked.
*/
if (isIncremental) {
for (GCZonesIter zone(rt); !zone.done(); zone.next())
@@ -3926,7 +3762,7 @@ GCRuntime::beginMarkPhase(JS::gcreason::Reason reason, AutoLockForExclusiveAcces
bufferGrayRoots();
markCompartments();
}
return true;
}
@@ -3965,9 +3801,9 @@ GCRuntime::markCompartments()
*/
/* Propagate the maybeAlive flag via cross-compartment edges. */
Vector<JSCompartment*, 0, js::SystemAllocPolicy> workList;
for (CompartmentsIter comp(rt, SkipAtoms); !comp.done(); comp.next()) {
if (comp->maybeAlive) {
if (!workList.append(comp))
@@ -3988,9 +3824,9 @@ GCRuntime::markCompartments()
}
}
/* Set scheduleForDestruction based on maybeAlive. */
for (GCCompartmentsIter comp(rt); !comp.done(); comp.next()) {
MOZ_ASSERT(!comp->scheduledForDestruction);
if (!comp->maybeAlive && !rt->isAtomsCompartment(comp))
@@ -4593,9 +4429,7 @@ MAKE_GC_SWEEP_TASK(SweepBaseShapesTask);
MAKE_GC_SWEEP_TASK(SweepInitialShapesTask);
MAKE_GC_SWEEP_TASK(SweepObjectGroupsTask);
MAKE_GC_SWEEP_TASK(SweepRegExpsTask);
MAKE_GC_SWEEP_TASK(SweepSavedStacksTask);
MAKE_GC_SWEEP_TASK(SweepSelfHostingScriptSourceTask);
MAKE_GC_SWEEP_TASK(SweepNativeIteratorsTask);
MAKE_GC_SWEEP_TASK(SweepMiscTask);
#undef MAKE_GC_SWEEP_TASK
/* virtual */ void
@@ -4628,25 +4462,13 @@ SweepRegExpsTask::run()
}
/* virtual */ void
SweepSavedStacksTask::run()
SweepMiscTask::run()
{
for (GCCompartmentGroupIter c(runtime); !c.done(); c.next()) {
c->sweepSavedStacks();
}
}
/* virtual */ void
SweepSelfHostingScriptSourceTask::run()
{
for (GCCompartmentGroupIter c(runtime); !c.done(); c.next())
c->sweepSelfHostingScriptSource();
}
/* virtual */ void
SweepNativeIteratorsTask::run()
{
for (GCCompartmentGroupIter c(runtime); !c.done(); c.next())
c->sweepNativeIterators();
}
}
void
@@ -4687,7 +4509,7 @@ PrepareWeakCacheTasks(JSRuntime* rt)
WeakCacheTaskVector out;
for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) {
for (JS::WeakCache<void*>* cache : zone->weakCaches_) {
if (!out.emplaceBack(rt, *cache)) {
if (!out.append(SweepWeakCacheTask(rt, *cache))) {
SweepWeakCachesFromMainThread(rt);
return WeakCacheTaskVector();
}
@@ -4729,9 +4551,7 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock)
SweepCCWrappersTask sweepCCWrappersTask(rt);
SweepObjectGroupsTask sweepObjectGroupsTask(rt);
SweepRegExpsTask sweepRegExpsTask(rt);
SweepSavedStacksTask sweepSavedStacksTask(rt);
SweepSelfHostingScriptSourceTask sweepSelfHostingScriptSourceTask(rt);
SweepNativeIteratorsTask sweepNativeIteratorsTask(rt);
SweepMiscTask sweepMiscTask(rt);
WeakCacheTaskVector sweepCacheTasks = PrepareWeakCacheTasks(rt);
for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) {
@@ -4779,9 +4599,7 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock)
startTask(sweepCCWrappersTask, gcstats::PHASE_SWEEP_CC_WRAPPER, helperLock);
startTask(sweepObjectGroupsTask, gcstats::PHASE_SWEEP_TYPE_OBJECT, helperLock);
startTask(sweepRegExpsTask, gcstats::PHASE_SWEEP_REGEXP, helperLock);
startTask(sweepSavedStacksTask, gcstats::PHASE_SWEEP_MISC, helperLock);
startTask(sweepSelfHostingScriptSourceTask, gcstats::PHASE_SWEEP_MISC, helperLock);
startTask(sweepNativeIteratorsTask, gcstats::PHASE_SWEEP_MISC, helperLock);
startTask(sweepMiscTask, gcstats::PHASE_SWEEP_MISC, helperLock);
for (auto& task : sweepCacheTasks)
startTask(task, gcstats::PHASE_SWEEP_MISC, helperLock);
}
@@ -4800,6 +4618,7 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock)
c->sweepJitCompartment(&fop);
c->sweepTemplateObjects();
}
for (GCZoneGroupIter zone(rt); !zone.done(); zone.next())
zone->sweepWeakMaps();
@@ -4859,9 +4678,7 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock)
joinTask(sweepCCWrappersTask, gcstats::PHASE_SWEEP_CC_WRAPPER, helperLock);
joinTask(sweepObjectGroupsTask, gcstats::PHASE_SWEEP_TYPE_OBJECT, helperLock);
joinTask(sweepRegExpsTask, gcstats::PHASE_SWEEP_REGEXP, helperLock);
joinTask(sweepSavedStacksTask, gcstats::PHASE_SWEEP_MISC, helperLock);
joinTask(sweepSelfHostingScriptSourceTask, gcstats::PHASE_SWEEP_MISC, helperLock);
joinTask(sweepNativeIteratorsTask, gcstats::PHASE_SWEEP_MISC, helperLock);
joinTask(sweepMiscTask, gcstats::PHASE_SWEEP_MISC, helperLock);
for (auto& task : sweepCacheTasks)
joinTask(task, gcstats::PHASE_SWEEP_MISC, helperLock);
}
@@ -5188,7 +5005,7 @@ GCRuntime::performSweepActions(SliceBudget& budget, AutoLockForExclusiveAccess&
// Reset phase index.
sweepPhaseIndex = 0;
endSweepingZoneGroup();
getNextZoneGroup();
if (!currentZoneGroup)
@@ -5290,7 +5107,6 @@ GCRuntime::compactPhase(JS::gcreason::Reason reason, SliceBudget& sliceBudget,
gcstats::AutoPhase ap(stats, gcstats::PHASE_COMPACT);
Arena* relocatedArenas = nullptr;
while (!zonesToMaybeCompact.isEmpty()) {
// TODO: JSScripts can move. If the sampler interrupts the GC in the
// middle of relocating an arena, invalid JSScript pointers may be
@@ -5742,7 +5558,7 @@ gc::AbortReason
gc::IsIncrementalGCUnsafe(JSRuntime* rt)
{
MOZ_ASSERT(!rt->mainThread.suppressGC);
if (rt->keepAtoms())
return gc::AbortReason::KeepAtomsSet;
@@ -5763,7 +5579,7 @@ GCRuntime::budgetIncrementalGC(JS::gcreason::Reason reason, SliceBudget& budget,
else if (mode != JSGC_MODE_INCREMENTAL)
unsafeReason = gc::AbortReason::ModeChange;
}
if (unsafeReason != AbortReason::None) {
resetIncrementalGC(unsafeReason, lock);
budget.makeUnlimited();
@@ -5771,7 +5587,7 @@ GCRuntime::budgetIncrementalGC(JS::gcreason::Reason reason, SliceBudget& budget,
return;
}
if (isTooMuchMalloc()) {
budget.makeUnlimited();
@@ -6011,12 +5827,6 @@ GCRuntime::checkCanCallAPI()
bool
GCRuntime::checkIfGCAllowedInCurrentState(JS::gcreason::Reason reason)
{
auto isTabCloseReason = [](JS::gcreason::Reason r) {
return r == JS::gcreason::PAGE_HIDE ||
r == JS::gcreason::POST_COMPARTMENT ||
r == JS::gcreason::NSJSCONTEXT_DESTROY;
};
if (rt->mainThread.suppressGC)
return false;
@@ -6025,12 +5835,6 @@ GCRuntime::checkIfGCAllowedInCurrentState(JS::gcreason::Reason reason)
if (rt->isBeingDestroyed() && !IsShutdownGC(reason))
return false;
// Allow fast tab-close cleanup even when the runtime is otherwise busy.
if (!isTabCloseReason(reason) &&
!IdleGCManager::shouldBypassIdleCheck(reason) &&
!idleGC.isIdleEnough())
return false;
return true;
}
@@ -6038,15 +5842,15 @@ bool
GCRuntime::shouldRepeatForDeadZone(JS::gcreason::Reason reason)
{
MOZ_ASSERT_IF(reason == JS::gcreason::COMPARTMENT_REVIVED, !isIncremental);
if (!isIncremental || isIncrementalGCInProgress())
return false;
for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) {
if (c->scheduledForDestruction)
return true;
}
return false;
}
@@ -6068,13 +5872,13 @@ GCRuntime::collect(bool nonincrementalByAPI, SliceBudget budget, JS::gcreason::R
do {
poked = false;
bool wasReset = gcCycle(nonincrementalByAPI, budget, reason);
bool repeatForDeadZone = false;
if (poked && cleanUpEverything) {
/* Need to re-schedule all zones for GC. */
JS::PrepareForFullGC(rt->contextFromMainThread());
} else if (shouldRepeatForDeadZone(reason) && !wasReset) {
/*
* This code makes an extra effort to collect compartments that we
@@ -6084,7 +5888,7 @@ GCRuntime::collect(bool nonincrementalByAPI, SliceBudget budget, JS::gcreason::R
repeatForDeadZone = true;
reason = JS::gcreason::COMPARTMENT_REVIVED;
}
/*
* If we reset an existing GC, we need to start a new one. Also, we
@@ -6108,16 +5912,8 @@ js::AutoEnqueuePendingParseTasksAfterGC::~AutoEnqueuePendingParseTasksAfterGC()
SliceBudget
GCRuntime::defaultBudget(JS::gcreason::Reason reason, int64_t millis)
{
auto isTabCloseReason = [](JS::gcreason::Reason r) {
return r == JS::gcreason::PAGE_HIDE ||
r == JS::gcreason::POST_COMPARTMENT ||
r == JS::gcreason::NSJSCONTEXT_DESTROY;
};
if (millis == 0) {
if (isTabCloseReason(reason))
millis = 3;
else if (reason == JS::gcreason::ALLOC_TRIGGER)
if (reason == JS::gcreason::ALLOC_TRIGGER)
millis = defaultSliceBudget();
else if (schedulingState.inHighFrequencyGCMode() && tunables.isDynamicMarkSliceEnabled())
millis = defaultSliceBudget() * IGC_MARK_SLICE_MULTIPLIER;
@@ -6205,18 +6001,6 @@ GCRuntime::notifyDidPaint()
interFrameGC = false;
}
void
GCRuntime::notifyJSExecutionStart()
{
idleGC.notifyJSExecutionStart();
}
void
GCRuntime::notifyJSExecutionEnd()
{
idleGC.notifyJSExecutionEnd();
}
static bool
ZonesSelected(JSRuntime* rt)
{
-1
View File
@@ -804,7 +804,6 @@ class ArenaLists
bool foregroundFinalize(FreeOp* fop, AllocKind thingKind, SliceBudget& sliceBudget,
SortedArenaList& sweepList);
void backgroundFinalizePhase(FreeOp* fop, const FinalizePhase& phase, Arena** empty);
static void backgroundFinalize(FreeOp* fop, Arena* listHead, Arena** empty);
// When finalizing arenas, whether to keep empty arenas on the list or
-1
View File
@@ -172,7 +172,6 @@ main_deunified_sources = [
'gc/Allocator.cpp',
'gc/Barrier.cpp',
'gc/GCTrace.cpp',
'gc/IdleGC.cpp',
'gc/Iteration.cpp',
'gc/Marking.cpp',
'gc/Memory.cpp',
+10 -11
View File
@@ -478,8 +478,7 @@ js::CancelOffThreadParses(JSRuntime* rt)
}
// Clean up any parse tasks which haven't been finished by the main thread.
GlobalHelperThreadState::ParseTaskVector& finished =
HelperThreadState().parseFinishedList(lock);
GlobalHelperThreadState::ParseTaskVector& finished = HelperThreadState().parseFinishedList(lock);
while (true) {
bool found = false;
for (size_t i = 0; i < finished.length(); i++) {
@@ -487,8 +486,7 @@ js::CancelOffThreadParses(JSRuntime* rt)
if (task->runtimeMatches(rt)) {
found = true;
AutoUnlockHelperThreadState unlock(lock);
HelperThreadState().cancelParseTask(rt->contextFromMainThread(),
task->kind, task);
HelperThreadState().cancelParseTask(rt->contextFromMainThread(), task->kind, task);
}
}
if (!found)
@@ -970,7 +968,8 @@ GlobalHelperThreadState::maxGCParallelThreads() const
bool
GlobalHelperThreadState::canStartWasmCompile(const AutoLockHelperThreadState& lock)
{
if (wasmWorklist(lock).empty())
// Don't execute an wasm job if an earlier one failed.
if (wasmWorklist(lock).empty() || numWasmFailedJobs)
return false;
// Honor the maximum allowed threads to compile wasm jobs at once,
@@ -1420,13 +1419,13 @@ HelperThread::handleWasmWorkload(AutoLockHelperThreadState& locked)
success = wasm::CompileFunction(task);
}
// Append the task to the finished queue owned by its module generator.
if (!success)
task->setFailed();
// On success, try to move work to the finished list.
if (success)
success = HelperThreadState().wasmFinishedList(locked).append(task);
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!task->finishedList()->append(task))
oomUnsafe.crash("HelperThread::handleWasmWorkload");
// On failure, note the failure for harvesting by the parent.
if (!success)
HelperThreadState().noteWasmFailure(locked);
// Notify the main thread in case it's waiting.
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
+2 -2
View File
@@ -89,8 +89,8 @@ class GlobalHelperThreadState
wasm::IonCompileTaskPtrVector wasmWorklist_, wasmFinishedList_;
public:
// Helper-thread initiated wasm compilations are serialized to avoid the
// deadlock scenario described in WasmGenerator.cpp.
// For now, only allow a single parallel wasm compilation to happen at a
// time. This avoids race conditions on wasmWorklist/wasmFinishedList/etc.
mozilla::Atomic<bool> wasmCompilationInProgress;
private:
-6
View File
@@ -673,12 +673,6 @@ JSRuntime::traceSharedIntlData(JSTracer* trc)
void
JSRuntime::triggerActivityCallback(bool active)
{
if (active) {
gc.notifyJSExecutionStart();
} else {
gc.notifyJSExecutionEnd();
}
if (!activityCallback)
return;
+31 -37
View File
@@ -54,7 +54,6 @@ ModuleGenerator::ModuleGenerator(ImportVector&& imports)
lastPatchedCallsite_(0),
startOfUnpatchedCallsites_(0),
parallel_(false),
parallelCompilationInProgressOnHelperThread_(false),
outstanding_(0),
activeFuncDef_(nullptr),
startedFuncDefs_(false),
@@ -72,21 +71,18 @@ ModuleGenerator::~ModuleGenerator()
AutoLockHelperThreadState lock;
while (true) {
IonCompileTaskPtrVector& worklist = HelperThreadState().wasmWorklist(lock);
for (size_t i = worklist.length(); i > 0;) {
if (worklist[i - 1]->finishedList() == &finishedTasks_) {
HelperThreadState().remove(worklist, &i);
MOZ_ASSERT(outstanding_ > 0);
outstanding_--;
} else {
i--;
}
}
MOZ_ASSERT(outstanding_ >= worklist.length());
outstanding_ -= worklist.length();
worklist.clear();
for (size_t i = finishedTasks_.length(); i > 0;) {
HelperThreadState().remove(finishedTasks_, &i);
MOZ_ASSERT(outstanding_ > 0);
outstanding_--;
}
IonCompileTaskPtrVector& finished = HelperThreadState().wasmFinishedList(lock);
MOZ_ASSERT(outstanding_ >= finished.length());
outstanding_ -= finished.length();
finished.clear();
uint32_t numFailed = HelperThreadState().harvestFailedWasmJobs(lock);
MOZ_ASSERT(outstanding_ >= numFailed);
outstanding_ -= numFailed;
if (!outstanding_)
break;
@@ -95,10 +91,8 @@ ModuleGenerator::~ModuleGenerator()
}
}
if (parallelCompilationInProgressOnHelperThread_) {
MOZ_ASSERT(HelperThreadState().wasmCompilationInProgress);
HelperThreadState().wasmCompilationInProgress = false;
}
MOZ_ASSERT(HelperThreadState().wasmCompilationInProgress);
HelperThreadState().wasmCompilationInProgress = false;
} else {
MOZ_ASSERT(!outstanding_);
}
@@ -214,9 +208,12 @@ ModuleGenerator::finishOutstandingTask()
while (true) {
MOZ_ASSERT(outstanding_ > 0);
if (!finishedTasks_.empty()) {
if (HelperThreadState().wasmFailed(lock))
return false;
if (!HelperThreadState().wasmFinishedList(lock).empty()) {
outstanding_--;
task = finishedTasks_.popCopy();
task = HelperThreadState().wasmFinishedList(lock).popCopy();
break;
}
@@ -224,9 +221,6 @@ ModuleGenerator::finishOutstandingTask()
}
}
if (task->failed())
return false;
return finishTask(task);
}
@@ -371,9 +365,6 @@ ModuleGenerator::patchFarJumps(const TrapExitOffsetArray& trapExits)
bool
ModuleGenerator::finishTask(IonCompileTask* task)
{
if (task->failed())
return false;
const FuncBytes& func = task->func();
FuncCompileResults& results = task->results();
@@ -865,25 +856,31 @@ ModuleGenerator::startFuncDefs()
MOZ_ASSERT(!startedFuncDefs_);
MOZ_ASSERT(!finishedFuncDefs_);
// Helper-thread initiated wasm compilations stay serialized so that we do
// not end up with multiple helper threads blocking on other helper threads.
// Main-thread compilations can still overlap because they drain their own
// finished-task queue and do not steal tasks from other generators.
// The wasmCompilationInProgress atomic ensures that there is only one
// parallel compilation in progress at a time. In the special case of
// asm.js, where the ModuleGenerator itself can be on a helper thread, this
// avoids the possibility of deadlock since at most 1 helper thread will be
// blocking on other helper threads and there are always >1 helper threads.
// With wasm, this restriction could be relaxed by moving the worklist state
// out of HelperThreadState since each independent compilation needs its own
// worklist pair. Alternatively, the deadlock could be avoided by having the
// ModuleGenerator thread make progress (on compile tasks) instead of
// blocking.
GlobalHelperThreadState& threads = HelperThreadState();
MOZ_ASSERT(threads.threadCount > 1);
uint32_t numTasks;
if (CanUseExtraThreads() && (!CurrentHelperThread() ||
threads.wasmCompilationInProgress.compareExchange(false, true))) {
if (CanUseExtraThreads() && threads.wasmCompilationInProgress.compareExchange(false, true)) {
#ifdef DEBUG
{
AutoLockHelperThreadState lock;
MOZ_ASSERT(!HelperThreadState().wasmFailed(lock));
MOZ_ASSERT(HelperThreadState().wasmWorklist(lock).empty());
MOZ_ASSERT(HelperThreadState().wasmFinishedList(lock).empty());
}
#endif
parallel_ = true;
parallelCompilationInProgressOnHelperThread_ = !!CurrentHelperThread();
numTasks = 2 * threads.maxWasmCompilationThreads();
} else {
numTasks = 1;
@@ -894,9 +891,6 @@ ModuleGenerator::startFuncDefs()
for (size_t i = 0; i < numTasks; i++)
tasks_.infallibleEmplaceBack(*shared_, COMPILATION_LIFO_DEFAULT_CHUNK_SIZE);
for (auto& task : tasks_)
task.setFinishedList(&finishedTasks_);
if (!freeTasks_.reserve(numTasks))
return false;
for (size_t i = 0; i < numTasks; i++)
-2
View File
@@ -105,11 +105,9 @@ class MOZ_STACK_CLASS ModuleGenerator
// Parallel compilation
bool parallel_;
bool parallelCompilationInProgressOnHelperThread_;
uint32_t outstanding_;
IonCompileTaskVector tasks_;
IonCompileTaskPtrVector freeTasks_;
IonCompileTaskPtrVector finishedTasks_;
// Assertions
DebugOnly<FunctionGenerator*> activeFuncDef_;
-23
View File
@@ -20,7 +20,6 @@
#define wasm_ion_compile_h
#include "jit/MacroAssembler.h"
#include "vm/HelperThreads.h"
#include "wasm/WasmTypes.h"
namespace js {
@@ -108,8 +107,6 @@ class IonCompileTask
UniqueFuncBytes func_;
CompileMode mode_;
Maybe<FuncCompileResults> results_;
IonCompileTaskPtrVector* finishedList_;
bool failed_;
IonCompileTask(const IonCompileTask&) = delete;
IonCompileTask& operator=(const IonCompileTask&) = delete;
@@ -117,26 +114,7 @@ class IonCompileTask
public:
IonCompileTask(const ModuleGeneratorData& mg, size_t defaultChunkSize)
: mg_(mg), lifo_(defaultChunkSize), func_(nullptr), mode_(CompileMode::None)
, finishedList_(nullptr)
, failed_(false)
{}
void setFinishedList(IonCompileTaskPtrVector* finishedList) {
finishedList_ = finishedList;
}
IonCompileTaskPtrVector* finishedList() const {
MOZ_ASSERT(finishedList_);
return finishedList_;
}
void setFailed() {
failed_ = true;
}
bool failed() const {
return failed_;
}
LifoAlloc& lifo() {
return lifo_;
}
@@ -166,7 +144,6 @@ class IonCompileTask
results_.reset();
lifo_.releaseAll();
mode_ = CompileMode::None;
failed_ = false;
}
};
+8 -11
View File
@@ -1737,7 +1737,9 @@ nsCSSRendering::PaintBackground(const PaintBGParams& aParams)
}
static bool
IsOpaqueBorderEdge(const nsStyleBorder& aBorder, mozilla::Side aSide)
IsOpaqueBorderEdge(const nsStyleBorder& aBorder,
const nsStyleColor& aColor,
mozilla::Side aSide)
{
if (aBorder.GetComputedBorder().Side(aSide) == 0)
return true;
@@ -1759,25 +1761,20 @@ IsOpaqueBorderEdge(const nsStyleBorder& aBorder, mozilla::Side aSide)
if (aBorder.mBorderImageSource.GetType() != eStyleImageType_Null)
return false;
StyleComplexColor color = aBorder.mBorderColor[aSide];
// We don't know the foreground color here, so if it's being used
// we must assume it might be transparent.
if (!color.IsNumericColor()) {
return false;
}
return NS_GET_A(color.mColor) == 255;
nscolor color = aColor.CalcComplexColor(aBorder.mBorderColor[aSide]);
return NS_GET_A(color) == 255;
}
/**
* Returns true if all border edges are either missing or opaque.
*/
static bool
IsOpaqueBorder(const nsStyleBorder& aBorder)
IsOpaqueBorder(const nsStyleBorder& aBorder, const nsStyleColor& aColor)
{
if (aBorder.mBorderColors)
return false;
NS_FOR_CSS_SIDES(i) {
if (!IsOpaqueBorderEdge(aBorder, i))
if (!IsOpaqueBorderEdge(aBorder, aColor, i))
return false;
}
return true;
@@ -1910,7 +1907,7 @@ nsCSSRendering::GetImageLayerClip(const nsStyleImageLayers::Layer& aLayer,
}
bool isSolidBorder =
aWillPaintBorder && IsOpaqueBorder(aBorder);
aWillPaintBorder && IsOpaqueBorder(aBorder, *aForFrame->StyleColor());
if (isSolidBorder && layerClip == StyleGeometryBox::Border) {
// If we have rounded corners, we need to inflate the background
// drawing area a bit to avoid seams between the border and
@@ -0,0 +1,18 @@
<!doctype html>
<meta charset="utf-8">
<style>
:root {
--color: #fff;
background: var(--color);
color: var(--color);
}
div {
display: block;
width: 51px;
height: 51px;
border-radius: 50%;
background-color: #323232;
border: 10px solid var(--color);
}
</style>
<div></div>
@@ -0,0 +1,18 @@
<!doctype html>
<meta charset="utf-8">
<style>
:root {
--color: #fff;
background: var(--color);
color: var(--color);
}
div {
display: block;
width: 51px;
height: 51px;
border-radius: 50%;
background-color: #323232;
border: 10px solid currentcolor;
}
</style>
<div></div>
@@ -89,6 +89,8 @@ fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)||/^Windows\x20NT\x206\.2/.te
# Test for antialiasing gaps between background and border
fuzzy-if(gtkWidget,1,9) fuzzy-if(winWidget&&!d2d,1,9) fuzzy-if(d2d,5,40) fuzzy-if(skiaContent,1,9) == curved-border-background-nogap.html curved-border-background-nogap-ref.html
== currentcolor-border-radius.html currentcolor-border-radius-ref.html
== color-layer-1a.html color-layer-1-ref.html
== corner-split.html corner-split-ref.svg # bug 1185636
+2 -1
View File
@@ -1273,7 +1273,8 @@ CSS_PROP_SHORTHAND(
border_width,
BorderWidth,
CSS_PROPERTY_PARSE_FUNCTION |
CSS_PROPERTY_UNITLESS_LENGTH_QUIRK,
CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH,
"")
CSS_PROP_POSITION(
bottom,
+60 -1
View File
@@ -3193,6 +3193,66 @@ nsComputedDOMStyle::DoGetBorderRightWidth()
return GetBorderWidthFor(eSideRight);
}
already_AddRefed<CSSValue>
nsComputedDOMStyle::DoGetBorderWidth()
{
nscoord widths[4];
if (mInnerFrame) {
AssertFlushedPendingReflows();
const nsMargin& usedBorder = mInnerFrame->GetUsedBorder();
NS_FOR_CSS_SIDES(side) {
widths[side] = usedBorder.Side(side);
}
} else {
const nsStyleBorder* border = StyleBorder();
NS_FOR_CSS_SIDES(side) {
widths[side] = border->GetComputedBorderWidth(side);
}
}
RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(false);
RefPtr<nsROCSSPrimitiveValue> top = new nsROCSSPrimitiveValue;
top->SetAppUnits(widths[eSideTop]);
valueList->AppendCSSValue(top.forget());
if (widths[eSideRight] == widths[eSideLeft]) {
if (widths[eSideTop] == widths[eSideBottom]) {
if (widths[eSideTop] == widths[eSideRight]) {
return valueList.forget();
}
RefPtr<nsROCSSPrimitiveValue> right = new nsROCSSPrimitiveValue;
right->SetAppUnits(widths[eSideRight]);
valueList->AppendCSSValue(right.forget());
return valueList.forget();
}
RefPtr<nsROCSSPrimitiveValue> right = new nsROCSSPrimitiveValue;
right->SetAppUnits(widths[eSideRight]);
valueList->AppendCSSValue(right.forget());
RefPtr<nsROCSSPrimitiveValue> bottom = new nsROCSSPrimitiveValue;
bottom->SetAppUnits(widths[eSideBottom]);
valueList->AppendCSSValue(bottom.forget());
return valueList.forget();
}
RefPtr<nsROCSSPrimitiveValue> right = new nsROCSSPrimitiveValue;
right->SetAppUnits(widths[eSideRight]);
valueList->AppendCSSValue(right.forget());
RefPtr<nsROCSSPrimitiveValue> bottom = new nsROCSSPrimitiveValue;
bottom->SetAppUnits(widths[eSideBottom]);
valueList->AppendCSSValue(bottom.forget());
RefPtr<nsROCSSPrimitiveValue> left = new nsROCSSPrimitiveValue;
left->SetAppUnits(widths[eSideLeft]);
valueList->AppendCSSValue(left.forget());
return valueList.forget();
}
already_AddRefed<CSSValue>
nsComputedDOMStyle::DoGetBorderTopColor()
{
@@ -6843,4 +6903,3 @@ nsComputedDOMStyle::DoGetOverflowBlockEnd()
return DoGetOverflowBlock();
}
+1
View File
@@ -344,6 +344,7 @@ private:
already_AddRefed<CSSValue> DoGetBorderBottomWidth();
already_AddRefed<CSSValue> DoGetBorderLeftWidth();
already_AddRefed<CSSValue> DoGetBorderRightWidth();
already_AddRefed<CSSValue> DoGetBorderWidth();
already_AddRefed<CSSValue> DoGetBorderTopColor();
already_AddRefed<CSSValue> DoGetBorderBottomColor();
already_AddRefed<CSSValue> DoGetBorderLeftColor();
@@ -95,7 +95,7 @@ COMPUTED_STYLE_PROP(border_top_left_radius, BorderTopLeftRadius)
COMPUTED_STYLE_PROP(border_top_right_radius, BorderTopRightRadius)
COMPUTED_STYLE_PROP(border_top_style, BorderTopStyle)
COMPUTED_STYLE_PROP(border_top_width, BorderTopWidth)
//// COMPUTED_STYLE_PROP(border_width, BorderWidth)
COMPUTED_STYLE_PROP(border_width, BorderWidth)
COMPUTED_STYLE_PROP(bottom, Bottom)
COMPUTED_STYLE_PROP(box_decoration_break, BoxDecorationBreak)
COMPUTED_STYLE_PROP(box_shadow, BoxShadow)
+128 -22
View File
@@ -319,6 +319,11 @@ nsRuleNode::EnsureInlineDisplay(StyleDisplay& display)
}
}
enum class AppUnitRounding {
Default,
TowardZero
};
static nscoord CalcLengthWith(const nsCSSValue& aValue,
nscoord aFontSize,
const nsStyleFont* aStyleFont,
@@ -326,7 +331,9 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
nsPresContext* aPresContext,
bool aUseProvidedRootEmSize,
bool aUseUserFontSet,
RuleNodeCacheConditions& aConditions);
RuleNodeCacheConditions& aConditions,
AppUnitRounding aRounding =
AppUnitRounding::Default);
struct CalcLengthCalcOps : public css::BasicCoordCalcOps,
public css::NumbersAlreadyNormalizedOps
@@ -339,18 +346,21 @@ struct CalcLengthCalcOps : public css::BasicCoordCalcOps,
const bool mUseProvidedRootEmSize;
const bool mUseUserFontSet;
RuleNodeCacheConditions& mConditions;
const AppUnitRounding mRounding;
CalcLengthCalcOps(nscoord aFontSize, const nsStyleFont* aStyleFont,
nsStyleContext* aStyleContext, nsPresContext* aPresContext,
bool aUseProvidedRootEmSize, bool aUseUserFontSet,
RuleNodeCacheConditions& aConditions)
RuleNodeCacheConditions& aConditions,
AppUnitRounding aRounding)
: mFontSize(aFontSize),
mStyleFont(aStyleFont),
mStyleContext(aStyleContext),
mPresContext(aPresContext),
mUseProvidedRootEmSize(aUseProvidedRootEmSize),
mUseUserFontSet(aUseUserFontSet),
mConditions(aConditions)
mConditions(aConditions),
mRounding(aRounding)
{
}
@@ -358,15 +368,84 @@ struct CalcLengthCalcOps : public css::BasicCoordCalcOps,
{
return CalcLengthWith(aValue, mFontSize, mStyleFont,
mStyleContext, mPresContext, mUseProvidedRootEmSize,
mUseUserFontSet, mConditions);
mUseUserFontSet, mConditions, mRounding);
}
};
static inline nscoord ScaleCoordRound(const nsCSSValue& aValue, float aFactor)
static inline nscoord
ScaleCoordWithRounding(const nsCSSValue& aValue, float aFactor,
AppUnitRounding aRounding)
{
if (aRounding == AppUnitRounding::TowardZero) {
return NSToCoordTruncClamped(aValue.GetFloatValue() * aFactor);
}
return NSToCoordRoundWithClamp(aValue.GetFloatValue() * aFactor);
}
static inline nscoord
CSSPixelsToAppUnitsWithRounding(float aPixels, AppUnitRounding aRounding)
{
if (aRounding == AppUnitRounding::TowardZero) {
return NSToCoordTruncClamped(double(aPixels) *
nsPresContext::AppUnitsPerCSSPixel());
}
return nsPresContext::CSSPixelsToAppUnits(aPixels);
}
static nscoord
GetFixedLengthWithRounding(const nsCSSValue& aValue,
nsPresContext* aPresContext,
AppUnitRounding aRounding)
{
if (aRounding == AppUnitRounding::Default) {
return aValue.GetFixedLength(aPresContext);
}
MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_PhysicalMillimeter,
"not a fixed length unit");
double inches = double(aValue.GetFloatValue()) / MM_PER_INCH_FLOAT;
return NSToCoordTruncClamped(
inches * aPresContext->DeviceContext()->AppUnitsPerPhysicalInch());
}
static nscoord
GetPixelLengthWithRounding(const nsCSSValue& aValue,
AppUnitRounding aRounding)
{
MOZ_ASSERT(aValue.IsPixelLengthUnit(), "not a pixel length unit");
double scaleFactor;
switch (aValue.GetUnit()) {
case eCSSUnit_Pixel:
return CSSPixelsToAppUnitsWithRounding(aValue.GetFloatValue(),
aRounding);
case eCSSUnit_Pica:
scaleFactor = 16.0;
break;
case eCSSUnit_Point:
scaleFactor = 4.0 / 3.0;
break;
case eCSSUnit_Inch:
scaleFactor = 96.0;
break;
case eCSSUnit_Millimeter:
scaleFactor = 96.0 / 25.4;
break;
case eCSSUnit_Centimeter:
scaleFactor = 96.0 / 2.54;
break;
case eCSSUnit_Quarter:
scaleFactor = 96.0 / 101.6;
break;
default:
NS_ERROR("should never get here");
return 0;
}
return CSSPixelsToAppUnitsWithRounding(
float(double(aValue.GetFloatValue()) * scaleFactor), aRounding);
}
static inline nscoord ScaleViewportCoordTrunc(const nsCSSValue& aValue,
nscoord aViewportSize)
{
@@ -465,7 +544,8 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
// except when called from
// CalcLengthWithInitialFont.
bool aUseUserFontSet,
RuleNodeCacheConditions& aConditions)
RuleNodeCacheConditions& aConditions,
AppUnitRounding aRounding)
{
NS_ASSERTION(aValue.IsLengthUnit() || aValue.IsCalcUnit(),
"not a length or calc unit");
@@ -476,10 +556,10 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
NS_ASSERTION(aPresContext, "Must have prescontext");
if (aValue.IsFixedLengthUnit()) {
return aValue.GetFixedLength(aPresContext);
return GetFixedLengthWithRounding(aValue, aPresContext, aRounding);
}
if (aValue.IsPixelLengthUnit()) {
return aValue.GetPixelLength();
return GetPixelLengthWithRounding(aValue, aRounding);
}
if (aValue.IsCalcUnit()) {
// For properties for which lengths are the *only* units accepted in
@@ -490,7 +570,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
CalcLengthCalcOps ops(aFontSize, aStyleFont,
aStyleContext, aPresContext,
aUseProvidedRootEmSize, aUseUserFontSet,
aConditions);
aConditions, aRounding);
return css::ComputeCalc(aValue, ops);
}
switch (aValue.GetUnit()) {
@@ -623,7 +703,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
rootFontSize = rootStyleFont->mFont.size;
}
return ScaleCoordRound(aValue, float(rootFontSize));
return ScaleCoordWithRounding(aValue, float(rootFontSize), aRounding);
}
default:
// Fall through to the code for units that can't be stored in the
@@ -648,7 +728,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
// CSS2.1 specifies that this unit scales to the computed font
// size, not the em-width in the font metrics, despite the name.
aConditions.SetFontSizeDependency(aFontSize);
return ScaleCoordRound(aValue, float(aFontSize));
return ScaleCoordWithRounding(aValue, float(aFontSize), aRounding);
}
case eCSSUnit_XHeight: {
aPresContext->SetUsesExChUnits(true);
@@ -656,7 +736,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
GetMetricsFor(aPresContext, aStyleContext, styleFont,
aFontSize, aUseUserFontSet);
aConditions.SetUncacheable();
return ScaleCoordRound(aValue, float(fm->XHeight()));
return ScaleCoordWithRounding(aValue, float(fm->XHeight()), aRounding);
}
case eCSSUnit_Char: {
aPresContext->SetUsesExChUnits(true);
@@ -668,8 +748,9 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue,
GetMetrics(fm->Orientation()).zeroOrAveCharWidth;
aConditions.SetUncacheable();
return ScaleCoordRound(aValue, ceil(aPresContext->AppUnitsPerDevPixel() *
zeroWidth));
return ScaleCoordWithRounding(
aValue, ceil(aPresContext->AppUnitsPerDevPixel() * zeroWidth),
aRounding);
}
default:
NS_NOTREACHED("unexpected unit");
@@ -701,6 +782,20 @@ static inline nscoord CalcLength(const nsCSSValue& aValue,
aPresContext, aConditions);
}
static inline nscoord
CalcLengthTowardZero(const nsCSSValue& aValue,
nsStyleContext* aStyleContext,
nsPresContext* aPresContext,
RuleNodeCacheConditions& aConditions)
{
NS_ASSERTION(aStyleContext, "Must have style data");
return CalcLengthWith(aValue, -1, nullptr,
aStyleContext, aPresContext,
false, true, aConditions,
AppUnitRounding::TowardZero);
}
/* static */ nscoord
nsRuleNode::CalcLengthWithInitialFont(nsPresContext* aPresContext,
const nsCSSValue& aValue)
@@ -8018,13 +8113,11 @@ nsRuleNode::ComputeBorderData(void* aStartStruct,
"Unexpected enum value");
border->SetBorderWidth(side,
(mPresContext->GetBorderWidthTable())[value.GetIntValue()]);
// OK to pass bad aParentCoord since we're not passing SETCOORD_INHERIT
} else if (SetCoord(value, coord, nsStyleCoord(),
SETCOORD_LENGTH | SETCOORD_CALC_LENGTH_ONLY,
aContext, mPresContext, conditions)) {
NS_ASSERTION(coord.GetUnit() == eStyleUnit_Coord, "unexpected unit");
} else if (value.IsLengthUnit() || value.IsCalcUnit()) {
// clamp negative calc() to 0.
border->SetBorderWidth(side, std::max(coord.GetCoordValue(), 0));
border->SetBorderWidth(side,
std::max(CalcLengthTowardZero(value, aContext, mPresContext,
conditions), 0));
} else if (eCSSUnit_Inherit == value.GetUnit()) {
conditions.SetUncacheable();
border->SetBorderWidth(side,
@@ -8302,6 +8395,11 @@ nsRuleNode::ComputeOutlineData(void* aStartStruct,
eCSSUnit_Revert == outlineWidthValue->GetUnit()) {
outline->mOutlineWidth =
nsStyleCoord(NS_STYLE_BORDER_WIDTH_MEDIUM, eStyleUnit_Enumerated);
} else if (outlineWidthValue->IsLengthUnit() ||
outlineWidthValue->IsCalcUnit()) {
outline->mOutlineWidth.SetCoordValue(
CalcLengthTowardZero(*outlineWidthValue, aContext, mPresContext,
conditions));
} else {
SetCoord(*outlineWidthValue, outline->mOutlineWidth,
parentOutline->mOutlineWidth,
@@ -8312,13 +8410,21 @@ nsRuleNode::ComputeOutlineData(void* aStartStruct,
// outline-offset: length, inherit
nsStyleCoord tempCoord;
const nsCSSValue* outlineOffsetValue = aRuleData->ValueForOutlineOffset();
if (SetCoord(*outlineOffsetValue, tempCoord,
if (outlineOffsetValue->IsLengthUnit() || outlineOffsetValue->IsCalcUnit()) {
outline->mOutlineOffset =
NS_ROUND_OFFSET_TO_PIXELS(
CalcLengthTowardZero(*outlineOffsetValue, aContext, mPresContext,
conditions),
mPresContext->AppUnitsPerDevPixel());
} else if (SetCoord(*outlineOffsetValue, tempCoord,
nsStyleCoord(parentOutline->mOutlineOffset,
nsStyleCoord::CoordConstructor),
SETCOORD_LH | SETCOORD_INITIAL_ZERO | SETCOORD_CALC_LENGTH_ONLY |
SETCOORD_UNSET_INITIAL,
aContext, mPresContext, conditions)) {
outline->mOutlineOffset = tempCoord.GetCoordValue();
outline->mOutlineOffset =
NS_ROUND_OFFSET_TO_PIXELS(tempCoord.GetCoordValue(),
mPresContext->AppUnitsPerDevPixel());
} else {
NS_ASSERTION(outlineOffsetValue->GetUnit() == eCSSUnit_Null,
"unexpected unit");
+7 -9
View File
@@ -1145,21 +1145,19 @@ private:
nsCSSShadowItem mArray[1]; // This MUST be the last item
};
// Border widths are rounded to the nearest integer number of pixels, but values
// between zero and one device pixels are always rounded up to one device pixel.
// Border widths are rounded down to integer pixels, but values between zero and
// one device pixel are always rounded up to one device pixel.
#define NS_ROUND_BORDER_TO_PIXELS(l,tpp) \
((l) == 0) ? 0 : std::max((tpp), ((l) + ((tpp) / 2)) / (tpp) * (tpp))
(((l) == 0) ? 0 : std::max((tpp), (l) / (tpp) * (tpp)))
// Caret widths are rounded to the nearest-below integer number of pixels, but values
// between zero and one device pixels are always rounded up to one device pixel.
#define NS_ROUND_CARET_TO_PIXELS(l,tpp) \
((l) == 0) ? 0 : std::max((tpp), (l) / (tpp) * (tpp))
// Outline offset is rounded to the nearest integer number of pixels, but values
// between zero and one device pixels are always rounded up to one device pixel.
// Note that the offset can be negative.
NS_ROUND_BORDER_TO_PIXELS(l,tpp)
// Outline offset is snapped like border widths, preserving the sign.
#define NS_ROUND_OFFSET_TO_PIXELS(l,tpp) \
(((l) == 0) ? 0 : \
((l) > 0) ? std::max( (tpp), ((l) + ((tpp) / 2)) / (tpp) * (tpp)) : \
std::min(-(tpp), ((l) - ((tpp) / 2)) / (tpp) * (tpp)))
(((l) > 0) ? NS_ROUND_BORDER_TO_PIXELS((l), (tpp)) : \
std::min(-(tpp), (l) / (tpp) * (tpp))))
// Returns if the given border style type is visible or not
static bool IsVisibleBorderStyle(uint8_t aStyle)
+1
View File
@@ -75,6 +75,7 @@ support-files = file_animations_with_disabled_properties.html
[test_nesting_flattening_parser_edges.html]
[test_nesting_flattening_recovery.html]
[test_box_size_keywords.html]
[test_border_width_rounding.html]
[test_bug73586.html]
[test_bug74880.html]
[test_bug98997.html]
@@ -0,0 +1,82 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test fractional border and outline width rounding</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<div id="display"></div>
<pre id="test">
<script class="testbody" type="text/javascript">
var display = document.getElementById("display");
var tests = [
["0px", "0px"],
["0.1px", "1px"],
["0.25px", "1px"],
["0.5px", "1px"],
["0.9px", "1px"],
["1px", "1px"],
["1.25px", "1px"],
["1.5px", "1px"],
["2px", "2px"],
["2.75px", "2px"],
["2.999px", "2px"],
["3px", "3px"],
["3.001px", "3px"],
];
function checkWidth(property, setupProperty, setupValue) {
tests.forEach(function(test) {
var specified = test[0];
var expected = test[1];
var div = document.createElement("div");
div.style[setupProperty] = setupValue;
div.style[property] = specified;
display.appendChild(div);
is(document.defaultView.getComputedStyle(div, "")[property], expected,
property + " computes " + specified + " as " + expected);
display.removeChild(div);
});
}
function checkOffset(property) {
tests.forEach(function(test) {
var specified = test[0];
var expected = test[1];
var div = document.createElement("div");
div.style[property] = specified;
display.appendChild(div);
is(document.defaultView.getComputedStyle(div, "")[property], expected,
property + " computes " + specified + " as " + expected);
display.removeChild(div);
});
tests.forEach(function(test) {
var specified = test[0];
var expected = test[0] == "0px" ? "0px" : "-" + test[1];
var div = document.createElement("div");
div.style[property] = "-" + specified;
display.appendChild(div);
is(document.defaultView.getComputedStyle(div, "")[property], expected,
property + " computes -" + specified + " as " + expected);
display.removeChild(div);
});
}
checkWidth("borderWidth", "borderStyle", "solid");
checkWidth("outlineWidth", "outlineStyle", "solid");
checkOffset("outlineOffset");
</script>
</pre>
</body>
</html>