mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-03-23 07:12:12 -07:00
Maintain a moving average of the mortality of each generation.
Copied from Perforce Change: 191081 ServerID: perforce.ravenbrook.com
This commit is contained in:
parent
473a6f2a82
commit
3174eba71e
15 changed files with 304 additions and 184 deletions
|
|
@ -470,7 +470,14 @@
|
|||
#define VM_ARENA_SIZE_DEFAULT ((Size)1 << 28)
|
||||
|
||||
|
||||
/* Stack configuration -- see <code/sp*.c> */
|
||||
/* Locus configuration -- see <code/locus.c> */
|
||||
|
||||
/* Weighting for the current observation, in the exponential moving
|
||||
* average computation of the mortality of a generation. */
|
||||
#define LocusMortalityALPHA (0.4)
|
||||
|
||||
|
||||
/* Stack probe configuration -- see <code/sp*.c> */
|
||||
|
||||
/* Currently StackProbe has a useful implementation only on Windows. */
|
||||
#if defined(MPS_OS_W3)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
#define EVENT_VERSION_MAJOR ((unsigned)1)
|
||||
#define EVENT_VERSION_MEDIAN ((unsigned)6)
|
||||
#define EVENT_VERSION_MINOR ((unsigned)0)
|
||||
#define EVENT_VERSION_MINOR ((unsigned)1)
|
||||
|
||||
|
||||
/* EVENT_LIST -- list of event types and general properties
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
*/
|
||||
|
||||
#define EventNameMAX ((size_t)19)
|
||||
#define EventCodeMAX ((EventCode)0x0087)
|
||||
#define EventCodeMAX ((EventCode)0x0088)
|
||||
|
||||
#define EVENT_LIST(EVENT, X) \
|
||||
/* 0123456789012345678 <- don't exceed without changing EventNameMAX */ \
|
||||
|
|
@ -189,11 +189,12 @@
|
|||
EVENT(X, AMCTraceEnd , 0x0081, TRUE, Trace) \
|
||||
EVENT(X, TraceCreatePoolGen , 0x0082, TRUE, Trace) \
|
||||
/* new events for performance analysis of large heaps. */ \
|
||||
EVENT(X, TraceCondemnZones , 0x0083, TRUE, Trace) \
|
||||
/* EVENT(X, TraceCondemnZones , 0x0083, TRUE, Trace) */ \
|
||||
EVENT(X, ArenaGenZoneAdd , 0x0084, TRUE, Arena) \
|
||||
EVENT(X, ArenaUseFreeZone , 0x0085, TRUE, Arena) \
|
||||
/* EVENT(X, ArenaBlacklistZone , 0x0086, TRUE, Arena) */ \
|
||||
EVENT(X, PauseTimeSet , 0x0087, TRUE, Arena)
|
||||
EVENT(X, PauseTimeSet , 0x0087, TRUE, Arena) \
|
||||
EVENT(X, TraceEndGen , 0x0088, TRUE, Trace)
|
||||
|
||||
|
||||
/* Remember to update EventNameMAX and EventCodeMAX above!
|
||||
|
|
@ -722,11 +723,6 @@
|
|||
PARAM(X, 9, W, newDeferredSize) /* new size (deferred) of pool gen */ \
|
||||
PARAM(X, 10, W, oldDeferredSize) /* old size (deferred) of pool gen */
|
||||
|
||||
#define EVENT_TraceCondemnZones_PARAMS(PARAM, X) \
|
||||
PARAM(X, 0, P, trace) /* the trace */ \
|
||||
PARAM(X, 1, W, condemnedSet) /* the condemned zoneSet */ \
|
||||
PARAM(X, 2, W, white) /* the trace's white zoneSet */
|
||||
|
||||
#define EVENT_ArenaGenZoneAdd_PARAMS(PARAM, X) \
|
||||
PARAM(X, 0, P, arena) /* the arena */ \
|
||||
PARAM(X, 1, P, gendesc) /* the generation description */ \
|
||||
|
|
@ -740,6 +736,14 @@
|
|||
PARAM(X, 0, P, arena) /* the arena */ \
|
||||
PARAM(X, 1, D, pauseTime) /* the new maximum pause time, in seconds */
|
||||
|
||||
#define EVENT_TraceEndGen_PARAMS(PARAM, X) \
|
||||
PARAM(X, 0, P, trace) /* the trace */ \
|
||||
PARAM(X, 1, P, gen) /* the generation */ \
|
||||
PARAM(X, 2, W, condemned) /* bytes condemned in generation */ \
|
||||
PARAM(X, 3, W, forwarded) /* bytes forwarded from generation */ \
|
||||
PARAM(X, 4, W, preservedInPlace) /* bytes preserved in generation */ \
|
||||
PARAM(X, 5, D, mortality) /* updated mortality */
|
||||
|
||||
|
||||
#endif /* eventdef_h */
|
||||
|
||||
|
|
|
|||
118
mps/code/locus.c
118
mps/code/locus.c
|
|
@ -123,8 +123,8 @@ static Bool GenParamCheck(GenParamStruct *params)
|
|||
{
|
||||
CHECKL(params != NULL);
|
||||
CHECKL(params->capacity > 0);
|
||||
CHECKL(params->mortality > 0.0);
|
||||
CHECKL(params->mortality < 1.0);
|
||||
CHECKL(params->mortality >= 0.0);
|
||||
CHECKL(params->mortality <= 1.0);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +174,79 @@ Size GenDescNewSize(GenDesc gen)
|
|||
}
|
||||
|
||||
|
||||
/* genDescTraceStart -- notify generation of start of a trace */
|
||||
|
||||
static void genDescStartTrace(GenDesc gen, Trace trace)
|
||||
{
|
||||
GenTraceStats stats;
|
||||
|
||||
AVERT(GenDesc, gen);
|
||||
AVERT(Trace, trace);
|
||||
|
||||
stats = &gen->trace[trace->ti];
|
||||
stats->condemned = 0;
|
||||
stats->forwarded = 0;
|
||||
stats->preservedInPlace = 0;
|
||||
}
|
||||
|
||||
|
||||
/* genDescEndTrace -- notify generation of end of a trace */
|
||||
|
||||
static void genDescEndTrace(GenDesc gen, Trace trace)
|
||||
{
|
||||
GenTraceStats stats;
|
||||
Size survived;
|
||||
|
||||
AVERT(GenDesc, gen);
|
||||
AVERT(Trace, trace);
|
||||
|
||||
stats = &gen->trace[trace->ti];
|
||||
survived = stats->forwarded + stats->preservedInPlace;
|
||||
AVER(survived <= stats->condemned);
|
||||
|
||||
if (stats->condemned > 0) {
|
||||
double mortality = 1.0 - survived / (double)stats->condemned;
|
||||
double alpha = LocusMortalityALPHA;
|
||||
gen->mortality = gen->mortality * (1 - alpha) + mortality * alpha;
|
||||
EVENT6(TraceEndGen, trace, gen, stats->condemned, stats->forwarded,
|
||||
stats->preservedInPlace, gen->mortality);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* GenDescCondemned -- memory in a generation was condemned for a trace */
|
||||
|
||||
void GenDescCondemned(GenDesc gen, Trace trace, Size size)
|
||||
{
|
||||
GenTraceStats stats;
|
||||
|
||||
AVERT(GenDesc, gen);
|
||||
AVERT(Trace, trace);
|
||||
|
||||
stats = &gen->trace[trace->ti];
|
||||
stats->condemned += size;
|
||||
trace->condemned += size;
|
||||
}
|
||||
|
||||
|
||||
/* GenDescSurvived -- memory in a generation survived a trace */
|
||||
|
||||
void GenDescSurvived(GenDesc gen, Trace trace, Size forwarded,
|
||||
Size preservedInPlace)
|
||||
{
|
||||
GenTraceStats stats;
|
||||
|
||||
AVERT(GenDesc, gen);
|
||||
AVERT(Trace, trace);
|
||||
|
||||
stats = &gen->trace[trace->ti];
|
||||
stats->forwarded += forwarded;
|
||||
stats->preservedInPlace += preservedInPlace;
|
||||
trace->forwardedSize += forwarded;
|
||||
trace->preservedInPlaceSize += preservedInPlace;
|
||||
}
|
||||
|
||||
|
||||
/* GenDescTotalSize -- return total size of generation */
|
||||
|
||||
Size GenDescTotalSize(GenDesc gen)
|
||||
|
|
@ -196,6 +269,7 @@ Size GenDescTotalSize(GenDesc gen)
|
|||
|
||||
Res GenDescDescribe(GenDesc gen, mps_lib_FILE *stream, Count depth)
|
||||
{
|
||||
Index i;
|
||||
Res res;
|
||||
Ring node, nextNode;
|
||||
|
||||
|
|
@ -213,6 +287,18 @@ Res GenDescDescribe(GenDesc gen, mps_lib_FILE *stream, Count depth)
|
|||
if (res != ResOK)
|
||||
return res;
|
||||
|
||||
for (i = 0; i < NELEMS(gen->trace); ++i) {
|
||||
GenTraceStats stats = &gen->trace[i];
|
||||
res = WriteF(stream, depth + 2,
|
||||
"trace $W {\n", (WriteFW)i,
|
||||
" condemned $W\n", (WriteFW)stats->condemned,
|
||||
" forwarded $W\n", (WriteFW)stats->forwarded,
|
||||
" preservedInPlace $W\n", (WriteFW)stats->preservedInPlace,
|
||||
"}\n", NULL);
|
||||
if (res != ResOK)
|
||||
return res;
|
||||
}
|
||||
|
||||
RING_FOR(node, &gen->locusRing, nextNode) {
|
||||
PoolGen pgen = RING_ELT(PoolGen, genRing, node);
|
||||
res = PoolGenDescribe(pgen, stream, depth + 2);
|
||||
|
|
@ -376,25 +462,35 @@ double ChainDeferral(Chain chain)
|
|||
}
|
||||
|
||||
|
||||
/* ChainStartGC -- called to notify start of GC for this chain */
|
||||
/* ChainStartTrace -- called to notify start of GC for this chain */
|
||||
|
||||
void ChainStartGC(Chain chain, Trace trace)
|
||||
void ChainStartTrace(Chain chain, Trace trace)
|
||||
{
|
||||
Index i;
|
||||
|
||||
AVERT(Chain, chain);
|
||||
AVERT(Trace, trace);
|
||||
|
||||
chain->activeTraces = TraceSetAdd(chain->activeTraces, trace);
|
||||
|
||||
for (i = 0; i < chain->genCount; ++i)
|
||||
genDescStartTrace(&chain->gens[i], trace);
|
||||
}
|
||||
|
||||
|
||||
/* ChainEndGC -- called to notify end of GC for this chain */
|
||||
/* ChainEndTrace -- called to notify end of GC for this chain */
|
||||
|
||||
void ChainEndGC(Chain chain, Trace trace)
|
||||
void ChainEndTrace(Chain chain, Trace trace)
|
||||
{
|
||||
Index i;
|
||||
|
||||
AVERT(Chain, chain);
|
||||
AVERT(Trace, trace);
|
||||
|
||||
chain->activeTraces = TraceSetDel(chain->activeTraces, trace);
|
||||
|
||||
for (i = 0; i < chain->genCount; ++i)
|
||||
genDescEndTrace(&chain->gens[i], trace);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -617,9 +713,10 @@ void PoolGenAccountForEmpty(PoolGen pgen, Size unused, Bool deferred)
|
|||
|
||||
/* PoolGenAccountForAge -- accounting for condemning
|
||||
*
|
||||
* Call this when memory is condemned via PoolWhiten. The size
|
||||
* parameter should be the amount of memory that is being condemned
|
||||
* for the first time. The deferred flag is as for PoolGenAccountForFill.
|
||||
* Call this when memory is condemned via PoolWhiten, or when
|
||||
* artificially ageing memory in PoolGenFree. The size parameter
|
||||
* should be the amount of memory that is being condemned for the
|
||||
* first time. The deferred flag is as for PoolGenAccountForFill.
|
||||
*
|
||||
* See <design/strategy/#accounting.op.age>
|
||||
*/
|
||||
|
|
@ -627,7 +724,8 @@ void PoolGenAccountForEmpty(PoolGen pgen, Size unused, Bool deferred)
|
|||
void PoolGenAccountForAge(PoolGen pgen, Size size, Bool deferred)
|
||||
{
|
||||
AVERT(PoolGen, pgen);
|
||||
|
||||
AVERT(Bool, deferred);
|
||||
|
||||
if (deferred) {
|
||||
AVER(pgen->newDeferredSize >= size);
|
||||
pgen->newDeferredSize -= size;
|
||||
|
|
|
|||
|
|
@ -17,11 +17,22 @@
|
|||
typedef struct GenParamStruct *GenParam;
|
||||
|
||||
typedef struct GenParamStruct {
|
||||
Size capacity; /* capacity in kB */
|
||||
double mortality;
|
||||
Size capacity; /* capacity in kB */
|
||||
double mortality; /* predicted mortality */
|
||||
} GenParamStruct;
|
||||
|
||||
|
||||
/* GenTraceStats -- per-generation per-trace statistics */
|
||||
|
||||
typedef struct GenTraceStatsStruct *GenTraceStats;
|
||||
|
||||
typedef struct GenTraceStatsStruct {
|
||||
Size condemned; /* size of objects condemned by the trace */
|
||||
Size forwarded; /* size of objects that were forwarded by the trace */
|
||||
Size preservedInPlace; /* size of objects preserved in place by the trace */
|
||||
} GenTraceStatsStruct;
|
||||
|
||||
|
||||
/* GenDesc -- descriptor of a generation in a chain */
|
||||
|
||||
typedef struct GenDescStruct *GenDesc;
|
||||
|
|
@ -30,11 +41,12 @@ typedef struct GenDescStruct *GenDesc;
|
|||
|
||||
typedef struct GenDescStruct {
|
||||
Sig sig;
|
||||
ZoneSet zones; /* zoneset for this generation */
|
||||
Size capacity; /* capacity in kB */
|
||||
double mortality;
|
||||
ZoneSet zones; /* zoneset for this generation */
|
||||
Size capacity; /* capacity in kB */
|
||||
double mortality; /* predicted mortality */
|
||||
RingStruct locusRing; /* Ring of all PoolGen's in this GenDesc (locus) */
|
||||
RingStruct segRing; /* Ring of GCSegs in this generation */
|
||||
GenTraceStatsStruct trace[TraceLIMIT];
|
||||
} GenDescStruct;
|
||||
|
||||
|
||||
|
|
@ -79,6 +91,8 @@ typedef struct mps_chain_s {
|
|||
extern Bool GenDescCheck(GenDesc gen);
|
||||
extern Size GenDescNewSize(GenDesc gen);
|
||||
extern Size GenDescTotalSize(GenDesc gen);
|
||||
extern void GenDescCondemned(GenDesc gen, Trace trace, Size size);
|
||||
extern void GenDescSurvived(GenDesc gen, Trace trace, Size forwarded, Size preservedInPlace);
|
||||
extern Res GenDescDescribe(GenDesc gen, mps_lib_FILE *stream, Count depth);
|
||||
|
||||
extern Res ChainCreate(Chain *chainReturn, Arena arena, size_t genCount,
|
||||
|
|
@ -87,8 +101,8 @@ extern void ChainDestroy(Chain chain);
|
|||
extern Bool ChainCheck(Chain chain);
|
||||
|
||||
extern double ChainDeferral(Chain chain);
|
||||
extern void ChainStartGC(Chain chain, Trace trace);
|
||||
extern void ChainEndGC(Chain chain, Trace trace);
|
||||
extern void ChainStartTrace(Chain chain, Trace trace);
|
||||
extern void ChainEndTrace(Chain chain, Trace trace);
|
||||
extern size_t ChainGens(Chain chain);
|
||||
extern GenDesc ChainGen(Chain chain, Index gen);
|
||||
extern Res ChainDescribe(Chain chain, mps_lib_FILE *stream, Count depth);
|
||||
|
|
|
|||
|
|
@ -440,9 +440,7 @@ typedef struct ScanStateStruct {
|
|||
STATISTIC_DECL(Count nailCount); /* segments nailed by ambig refs */
|
||||
STATISTIC_DECL(Count snapCount); /* refs snapped to forwarded objs */
|
||||
STATISTIC_DECL(Count forwardedCount); /* objects preserved by moving */
|
||||
Size forwardedSize; /* bytes preserved by moving */
|
||||
STATISTIC_DECL(Count preservedInPlaceCount); /* objects preserved in place */
|
||||
Size preservedInPlaceSize; /* bytes preserved in place */
|
||||
STATISTIC_DECL(Size copiedSize); /* bytes copied */
|
||||
STATISTIC_DECL(Size scannedSize); /* bytes scanned */
|
||||
} ScanStateStruct;
|
||||
|
|
|
|||
|
|
@ -326,13 +326,13 @@ Bool PolicyStartTrace(Trace *traceReturn, Arena arena)
|
|||
|
||||
res = TraceCreate(&trace, arena, TraceStartWhyCHAIN_GEN0CAP);
|
||||
AVER(res == ResOK);
|
||||
trace->chain = firstChain;
|
||||
ChainStartTrace(firstChain, trace);
|
||||
res = policyCondemnChain(&mortality, firstChain, trace);
|
||||
if (res != ResOK) /* should try some other trace, really @@@@ */
|
||||
goto failCondemn;
|
||||
if (TraceIsEmpty(trace))
|
||||
goto nothingCondemned;
|
||||
trace->chain = firstChain;
|
||||
ChainStartGC(firstChain, trace);
|
||||
res = TraceStart(trace, mortality, trace->condemned * TraceWorkFactor);
|
||||
/* We don't expect normal GC traces to fail to start. */
|
||||
AVER(res == ResOK);
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ typedef struct amcSegStruct {
|
|||
GCSegStruct gcSegStruct; /* superclass fields must come first */
|
||||
amcGen gen; /* generation this segment belongs to */
|
||||
Nailboard board; /* nailboard for this segment or NULL if none */
|
||||
Size forwarded[TraceLIMIT]; /* size of objects forwarded for each trace */
|
||||
BOOLFIELD(old); /* .seg.old */
|
||||
BOOLFIELD(deferred); /* .seg.deferred */
|
||||
Sig sig; /* <code/misc.h#sig> */
|
||||
|
|
@ -1207,10 +1208,6 @@ static Res AMCWhiten(Pool pool, Trace trace, Seg seg)
|
|||
}
|
||||
}
|
||||
|
||||
SegSetWhite(seg, TraceSetAdd(SegWhite(seg), trace));
|
||||
condemned += SegSize(seg);
|
||||
trace->condemned += condemned;
|
||||
|
||||
amc = PoolAMC(pool);
|
||||
AVERT(AMC, amc);
|
||||
|
||||
|
|
@ -1221,6 +1218,10 @@ static Res AMCWhiten(Pool pool, Trace trace, Seg seg)
|
|||
amcseg->old = TRUE;
|
||||
}
|
||||
|
||||
amcseg->forwarded[trace->ti] = 0;
|
||||
SegSetWhite(seg, TraceSetAdd(SegWhite(seg), trace));
|
||||
GenDescCondemned(gen->pgen.gen, trace, condemned + SegSize(seg));
|
||||
|
||||
/* Ensure we are forwarding into the right generation. */
|
||||
|
||||
/* see <design/poolamc/#gen.ramp> */
|
||||
|
|
@ -1635,7 +1636,6 @@ static Res AMCFix(Pool pool, ScanState ss, Seg seg, Ref *refIO)
|
|||
|
||||
length = AddrOffset(ref, clientQ); /* .exposed.seg */
|
||||
STATISTIC_STAT(++ss->forwardedCount);
|
||||
ss->forwardedSize += length;
|
||||
do {
|
||||
res = BUFFER_RESERVE(&newBase, buffer, length);
|
||||
if (res != ResOK)
|
||||
|
|
@ -1695,6 +1695,7 @@ static void amcReclaimNailed(Pool pool, Trace trace, Seg seg)
|
|||
Count preservedInPlaceCount = (Count)0;
|
||||
Size preservedInPlaceSize = (Size)0;
|
||||
AMC amc;
|
||||
PoolGen pgen;
|
||||
Size headerSize;
|
||||
Addr padBase; /* base of next padding object */
|
||||
Size padLength; /* length of next padding object */
|
||||
|
|
@ -1773,19 +1774,19 @@ static void amcReclaimNailed(Pool pool, Trace trace, Seg seg)
|
|||
AVER(bytesReclaimed <= SegSize(seg));
|
||||
trace->reclaimSize += bytesReclaimed;
|
||||
trace->preservedInPlaceCount += preservedInPlaceCount;
|
||||
trace->preservedInPlaceSize += preservedInPlaceSize;
|
||||
pgen = &amcSegGen(seg)->pgen;
|
||||
GenDescSurvived(pgen->gen, trace, Seg2amcSeg(seg)->forwarded[trace->ti],
|
||||
preservedInPlaceSize);
|
||||
|
||||
/* Free the seg if we can; fixes .nailboard.limitations.middle. */
|
||||
if(preservedInPlaceCount == 0
|
||||
&& (SegBuffer(seg) == NULL)
|
||||
&& (SegNailed(seg) == TraceSetEMPTY)) {
|
||||
|
||||
amcGen gen = amcSegGen(seg);
|
||||
|
||||
/* We may not free a buffered seg. */
|
||||
AVER(SegBuffer(seg) == NULL);
|
||||
|
||||
PoolGenFree(&gen->pgen, seg, 0, SegSize(seg), 0, Seg2amcSeg(seg)->deferred);
|
||||
PoolGenFree(pgen, seg, 0, SegSize(seg), 0, Seg2amcSeg(seg)->deferred);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1832,6 +1833,7 @@ static void AMCReclaim(Pool pool, Trace trace, Seg seg)
|
|||
|
||||
trace->reclaimSize += SegSize(seg);
|
||||
|
||||
GenDescSurvived(gen->pgen.gen, trace, Seg2amcSeg(seg)->forwarded[trace->ti], 0);
|
||||
PoolGenFree(&gen->pgen, seg, 0, SegSize(seg), 0, Seg2amcSeg(seg)->deferred);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1078,7 +1078,7 @@ static Res AMSWhiten(Pool pool, Trace trace, Seg seg)
|
|||
AMS ams;
|
||||
AMSSeg amsseg;
|
||||
Buffer buffer; /* the seg's buffer, if it has one */
|
||||
Count uncondemned;
|
||||
Count uncondemnedGrains, condemnedGrains;
|
||||
|
||||
AVERT(Pool, pool);
|
||||
ams = PoolAMS(pool);
|
||||
|
|
@ -1128,21 +1128,23 @@ static Res AMSWhiten(Pool pool, Trace trace, Seg seg)
|
|||
AMS_RANGE_BLACKEN(seg, scanLimitIndex, limitIndex);
|
||||
amsRangeWhiten(seg, limitIndex, amsseg->grains);
|
||||
/* We didn't condemn the buffer, subtract it from the count. */
|
||||
uncondemned = limitIndex - scanLimitIndex;
|
||||
uncondemnedGrains = limitIndex - scanLimitIndex;
|
||||
} else { /* condemn whole seg */
|
||||
amsRangeWhiten(seg, 0, amsseg->grains);
|
||||
uncondemned = (Count)0;
|
||||
uncondemnedGrains = (Count)0;
|
||||
}
|
||||
|
||||
/* The unused part of the buffer remains new: the rest becomes old. */
|
||||
PoolGenAccountForAge(&ams->pgen, AMSGrainsSize(ams, amsseg->newGrains - uncondemned), FALSE);
|
||||
amsseg->oldGrains += amsseg->newGrains - uncondemned;
|
||||
amsseg->newGrains = uncondemned;
|
||||
condemnedGrains = amsseg->newGrains - uncondemnedGrains;
|
||||
PoolGenAccountForAge(&ams->pgen, AMSGrainsSize(ams, condemnedGrains), FALSE);
|
||||
amsseg->oldGrains += condemnedGrains;
|
||||
amsseg->newGrains = uncondemnedGrains;
|
||||
amsseg->marksChanged = FALSE; /* <design/poolams/#marked.condemn> */
|
||||
amsseg->ambiguousFixes = FALSE;
|
||||
|
||||
if (amsseg->oldGrains > 0) {
|
||||
trace->condemned += AMSGrainsSize(ams, amsseg->oldGrains);
|
||||
GenDescCondemned(ams->pgen.gen, trace,
|
||||
AMSGrainsSize(ams, amsseg->oldGrains));
|
||||
SegSetWhite(seg, TraceSetAdd(SegWhite(seg), trace));
|
||||
} else {
|
||||
amsseg->colourTablesInUse = FALSE;
|
||||
|
|
@ -1550,6 +1552,7 @@ static void AMSReclaim(Pool pool, Trace trace, Seg seg)
|
|||
AMS ams;
|
||||
AMSSeg amsseg;
|
||||
Count nowFree, grains, reclaimedGrains;
|
||||
Size preservedInPlaceSize;
|
||||
PoolDebugMixin debug;
|
||||
|
||||
AVERT(Pool, pool);
|
||||
|
|
@ -1601,7 +1604,8 @@ static void AMSReclaim(Pool pool, Trace trace, Seg seg)
|
|||
PoolGenAccountForReclaim(&ams->pgen, AMSGrainsSize(ams, reclaimedGrains), FALSE);
|
||||
trace->reclaimSize += AMSGrainsSize(ams, reclaimedGrains);
|
||||
/* preservedInPlaceCount is updated on fix */
|
||||
trace->preservedInPlaceSize += AMSGrainsSize(ams, amsseg->oldGrains);
|
||||
preservedInPlaceSize = AMSGrainsSize(ams, amsseg->oldGrains);
|
||||
GenDescSurvived(ams->pgen.gen, trace, 0, preservedInPlaceSize);
|
||||
|
||||
/* Ensure consistency of segment even if are just about to free it */
|
||||
amsseg->colourTablesInUse = FALSE;
|
||||
|
|
|
|||
|
|
@ -753,7 +753,7 @@ static Res AWLWhiten(Pool pool, Trace trace, Seg seg)
|
|||
AWL awl;
|
||||
AWLSeg awlseg;
|
||||
Buffer buffer;
|
||||
Count uncondemned;
|
||||
Count uncondemnedGrains, condemnedGrains;
|
||||
|
||||
/* All parameters checked by generic PoolWhiten. */
|
||||
|
||||
|
|
@ -769,13 +769,13 @@ static Res AWLWhiten(Pool pool, Trace trace, Seg seg)
|
|||
|
||||
if(buffer == NULL) {
|
||||
awlRangeWhiten(awlseg, 0, awlseg->grains);
|
||||
uncondemned = (Count)0;
|
||||
uncondemnedGrains = (Count)0;
|
||||
} else {
|
||||
/* Whiten everything except the buffer. */
|
||||
Addr base = SegBase(seg);
|
||||
Index scanLimitIndex = awlIndexOfAddr(base, awl, BufferScanLimit(buffer));
|
||||
Index limitIndex = awlIndexOfAddr(base, awl, BufferLimit(buffer));
|
||||
uncondemned = limitIndex - scanLimitIndex;
|
||||
uncondemnedGrains = limitIndex - scanLimitIndex;
|
||||
awlRangeWhiten(awlseg, 0, scanLimitIndex);
|
||||
awlRangeWhiten(awlseg, limitIndex, awlseg->grains);
|
||||
|
||||
|
|
@ -788,12 +788,14 @@ static Res AWLWhiten(Pool pool, Trace trace, Seg seg)
|
|||
}
|
||||
}
|
||||
|
||||
PoolGenAccountForAge(&awl->pgen, AWLGrainsSize(awl, awlseg->newGrains - uncondemned), FALSE);
|
||||
awlseg->oldGrains += awlseg->newGrains - uncondemned;
|
||||
awlseg->newGrains = uncondemned;
|
||||
condemnedGrains = awlseg->newGrains - uncondemnedGrains;
|
||||
PoolGenAccountForAge(&awl->pgen, AWLGrainsSize(awl, condemnedGrains), FALSE);
|
||||
awlseg->oldGrains += condemnedGrains;
|
||||
awlseg->newGrains = uncondemnedGrains;
|
||||
|
||||
if (awlseg->oldGrains > 0) {
|
||||
trace->condemned += AWLGrainsSize(awl, awlseg->oldGrains);
|
||||
GenDescCondemned(awl->pgen.gen, trace,
|
||||
AWLGrainsSize(awl, awlseg->oldGrains));
|
||||
SegSetWhite(seg, TraceSetAdd(SegWhite(seg), trace));
|
||||
}
|
||||
|
||||
|
|
@ -1172,7 +1174,7 @@ static void AWLReclaim(Pool pool, Trace trace, Seg seg)
|
|||
|
||||
trace->reclaimSize += AWLGrainsSize(awl, reclaimedGrains);
|
||||
trace->preservedInPlaceCount += preservedInPlaceCount;
|
||||
trace->preservedInPlaceSize += preservedInPlaceSize;
|
||||
GenDescSurvived(awl->pgen.gen, trace, 0, preservedInPlaceSize);
|
||||
SegSetWhite(seg, TraceSetDel(SegWhite(seg), trace));
|
||||
|
||||
if (awlseg->freeGrains == awlseg->grains && buffer == NULL)
|
||||
|
|
|
|||
|
|
@ -381,8 +381,7 @@ static void loSegReclaim(LOSeg loseg, Trace trace)
|
|||
|
||||
trace->reclaimSize += LOGrainsSize(lo, reclaimedGrains);
|
||||
trace->preservedInPlaceCount += preservedInPlaceCount;
|
||||
trace->preservedInPlaceSize += preservedInPlaceSize;
|
||||
|
||||
GenDescSurvived(lo->pgen.gen, trace, 0, preservedInPlaceSize);
|
||||
SegSetWhite(seg, TraceSetDel(SegWhite(seg), trace));
|
||||
|
||||
if (!marked)
|
||||
|
|
@ -671,7 +670,7 @@ static Res LOWhiten(Pool pool, Trace trace, Seg seg)
|
|||
LO lo;
|
||||
LOSeg loseg;
|
||||
Buffer buffer;
|
||||
Count grains, uncondemned;
|
||||
Count grains, uncondemnedGrains, condemnedGrains;
|
||||
|
||||
AVERT(Pool, pool);
|
||||
lo = PoolPoolLO(pool);
|
||||
|
|
@ -691,21 +690,25 @@ static Res LOWhiten(Pool pool, Trace trace, Seg seg)
|
|||
Addr base = SegBase(seg);
|
||||
Index scanLimitIndex = loIndexOfAddr(base, lo, BufferScanLimit(buffer));
|
||||
Index limitIndex = loIndexOfAddr(base, lo, BufferLimit(buffer));
|
||||
uncondemned = limitIndex - scanLimitIndex;
|
||||
uncondemnedGrains = limitIndex - scanLimitIndex;
|
||||
if (0 < scanLimitIndex)
|
||||
BTCopyInvertRange(loseg->alloc, loseg->mark, 0, scanLimitIndex);
|
||||
if (limitIndex < grains)
|
||||
BTCopyInvertRange(loseg->alloc, loseg->mark, limitIndex, grains);
|
||||
} else {
|
||||
uncondemned = (Count)0;
|
||||
uncondemnedGrains = (Count)0;
|
||||
BTCopyInvertRange(loseg->alloc, loseg->mark, 0, grains);
|
||||
}
|
||||
|
||||
PoolGenAccountForAge(&lo->pgen, LOGrainsSize(lo, loseg->newGrains - uncondemned), FALSE);
|
||||
loseg->oldGrains += loseg->newGrains - uncondemned;
|
||||
loseg->newGrains = uncondemned;
|
||||
trace->condemned += LOGrainsSize(lo, loseg->oldGrains);
|
||||
SegSetWhite(seg, TraceSetAdd(SegWhite(seg), trace));
|
||||
condemnedGrains = loseg->newGrains - uncondemnedGrains;
|
||||
PoolGenAccountForAge(&lo->pgen, LOGrainsSize(lo, condemnedGrains), FALSE);
|
||||
loseg->oldGrains += condemnedGrains;
|
||||
loseg->newGrains = uncondemnedGrains;
|
||||
|
||||
if (loseg->oldGrains > 0) {
|
||||
GenDescCondemned(lo->pgen.gen, trace, LOGrainsSize(lo, loseg->oldGrains));
|
||||
SegSetWhite(seg, TraceSetAdd(SegWhite(seg), trace));
|
||||
}
|
||||
|
||||
return ResOK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,9 +105,7 @@ void ScanStateInit(ScanState ss, TraceSet ts, Arena arena,
|
|||
STATISTIC(ss->nailCount = (Count)0);
|
||||
STATISTIC(ss->snapCount = (Count)0);
|
||||
STATISTIC(ss->forwardedCount = (Count)0);
|
||||
ss->forwardedSize = (Size)0; /* see .message.data */
|
||||
STATISTIC(ss->preservedInPlaceCount = (Count)0);
|
||||
ss->preservedInPlaceSize = (Size)0; /* see .message.data */
|
||||
STATISTIC(ss->copiedSize = (Size)0);
|
||||
ss->scannedSize = (Size)0; /* see .work */
|
||||
ss->sig = ScanStateSig;
|
||||
|
|
@ -296,11 +294,7 @@ static void traceUpdateCounts(Trace trace, ScanState ss,
|
|||
STATISTIC(trace->nailCount += ss->nailCount);
|
||||
STATISTIC(trace->snapCount += ss->snapCount);
|
||||
STATISTIC(trace->forwardedCount += ss->forwardedCount);
|
||||
trace->forwardedSize += ss->forwardedSize; /* see .message.data */
|
||||
STATISTIC(trace->preservedInPlaceCount += ss->preservedInPlaceCount);
|
||||
trace->preservedInPlaceSize += ss->preservedInPlaceSize;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -741,6 +735,33 @@ found:
|
|||
}
|
||||
|
||||
|
||||
/* traceDestroyCommon -- common functionality for TraceDestroy* */
|
||||
|
||||
static void traceDestroyCommon(Trace trace)
|
||||
{
|
||||
Ring chainNode, nextChainNode;
|
||||
|
||||
if (trace->chain != NULL) {
|
||||
ChainEndTrace(trace->chain, trace);
|
||||
} else {
|
||||
/* Notify all the chains. */
|
||||
RING_FOR(chainNode, &trace->arena->chainRing, nextChainNode) {
|
||||
Chain chain = RING_ELT(Chain, chainRing, chainNode);
|
||||
ChainEndTrace(chain, trace);
|
||||
}
|
||||
}
|
||||
|
||||
EVENT1(TraceDestroy, trace);
|
||||
|
||||
trace->sig = SigInvalid;
|
||||
trace->arena->busyTraces = TraceSetDel(trace->arena->busyTraces, trace);
|
||||
trace->arena->flippedTraces = TraceSetDel(trace->arena->flippedTraces, trace);
|
||||
|
||||
/* Hopefully the trace reclaimed some memory, so clear any emergency. */
|
||||
ArenaSetEmergency(trace->arena, FALSE);
|
||||
}
|
||||
|
||||
|
||||
/* TraceDestroyInit -- destroy a trace object in state INIT */
|
||||
|
||||
void TraceDestroyInit(Trace trace)
|
||||
|
|
@ -748,14 +769,9 @@ void TraceDestroyInit(Trace trace)
|
|||
AVERT(Trace, trace);
|
||||
AVER(trace->state == TraceINIT);
|
||||
AVER(trace->condemned == 0);
|
||||
AVER(!TraceSetIsMember(trace->arena->flippedTraces, trace));
|
||||
|
||||
EVENT1(TraceDestroy, trace);
|
||||
|
||||
trace->sig = SigInvalid;
|
||||
trace->arena->busyTraces = TraceSetDel(trace->arena->busyTraces, trace);
|
||||
|
||||
/* Clear the emergency flag so the next trace starts normally. */
|
||||
ArenaSetEmergency(trace->arena, FALSE);
|
||||
traceDestroyCommon(trace);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -773,19 +789,6 @@ void TraceDestroyFinished(Trace trace)
|
|||
AVERT(Trace, trace);
|
||||
AVER(trace->state == TraceFINISHED);
|
||||
|
||||
if(trace->chain == NULL) {
|
||||
Ring chainNode, nextChainNode;
|
||||
|
||||
/* Notify all the chains. */
|
||||
RING_FOR(chainNode, &trace->arena->chainRing, nextChainNode) {
|
||||
Chain chain = RING_ELT(Chain, chainRing, chainNode);
|
||||
|
||||
ChainEndGC(chain, trace);
|
||||
}
|
||||
} else {
|
||||
ChainEndGC(trace->chain, trace);
|
||||
}
|
||||
|
||||
STATISTIC_STAT(EVENT13
|
||||
(TraceStatScan, trace,
|
||||
trace->rootScanCount, trace->rootScanSize,
|
||||
|
|
@ -808,14 +811,7 @@ void TraceDestroyFinished(Trace trace)
|
|||
(TraceStatReclaim, trace,
|
||||
trace->reclaimCount, trace->reclaimSize));
|
||||
|
||||
EVENT1(TraceDestroy, trace);
|
||||
|
||||
trace->sig = SigInvalid;
|
||||
trace->arena->busyTraces = TraceSetDel(trace->arena->busyTraces, trace);
|
||||
trace->arena->flippedTraces = TraceSetDel(trace->arena->flippedTraces, trace);
|
||||
|
||||
/* Hopefully the trace reclaimed some memory, so clear any emergency. */
|
||||
ArenaSetEmergency(trace->arena, FALSE);
|
||||
traceDestroyCommon(trace);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1490,7 +1486,7 @@ static Res traceCondemnAll(Trace trace)
|
|||
{
|
||||
Res res;
|
||||
Arena arena;
|
||||
Ring poolNode, nextPoolNode, chainNode, nextChainNode;
|
||||
Ring poolNode, nextPoolNode;
|
||||
|
||||
arena = trace->arena;
|
||||
AVERT(Arena, arena);
|
||||
|
|
@ -1522,12 +1518,6 @@ static Res traceCondemnAll(Trace trace)
|
|||
if (TraceIsEmpty(trace))
|
||||
return ResFAIL;
|
||||
|
||||
/* Notify all the chains. */
|
||||
RING_FOR(chainNode, &arena->chainRing, nextChainNode) {
|
||||
Chain chain = RING_ELT(Chain, chainRing, chainNode);
|
||||
|
||||
ChainStartGC(chain, trace);
|
||||
}
|
||||
return ResOK;
|
||||
|
||||
failBegin:
|
||||
|
|
@ -1749,12 +1739,20 @@ Res TraceStartCollectAll(Trace *traceReturn, Arena arena, int why)
|
|||
Trace trace = NULL;
|
||||
Res res;
|
||||
double finishingTime;
|
||||
Ring chainNode, nextChainNode;
|
||||
|
||||
AVERT(Arena, arena);
|
||||
AVER(arena->busyTraces == TraceSetEMPTY);
|
||||
|
||||
res = TraceCreate(&trace, arena, why);
|
||||
AVER(res == ResOK); /* succeeds because no other trace is busy */
|
||||
|
||||
/* Notify all the chains. */
|
||||
RING_FOR(chainNode, &arena->chainRing, nextChainNode) {
|
||||
Chain chain = RING_ELT(Chain, chainRing, chainNode);
|
||||
ChainStartTrace(chain, trace);
|
||||
}
|
||||
|
||||
res = traceCondemnAll(trace);
|
||||
if(res != ResOK) /* should try some other trace, really @@@@ */
|
||||
goto failCondemn;
|
||||
|
|
|
|||
|
|
@ -34,45 +34,32 @@ short-lived objects.)
|
|||
First, the effect of varying the capacity of a chain with a single
|
||||
generation.
|
||||
|
||||
======== ========= =========================
|
||||
Capacity Mortality Execution time (user+sys)
|
||||
======== ========= =========================
|
||||
100 0.80 362.6
|
||||
200 0.80 354.9
|
||||
400 0.80 349.7
|
||||
800 0.80 314.4
|
||||
1600 0.80 215.7
|
||||
3200 0.80 94.0
|
||||
6400 0.80 53.5
|
||||
12800 0.80 79.6
|
||||
25600 0.80 77.6
|
||||
======== ========= =========================
|
||||
======== =========================
|
||||
Capacity Execution time (user+sys)
|
||||
======== =========================
|
||||
100 362.6
|
||||
200 354.9
|
||||
400 349.7
|
||||
800 314.4
|
||||
1600 215.7
|
||||
3200 94.0
|
||||
6400 53.5
|
||||
12800 79.6
|
||||
25600 77.6
|
||||
======== =========================
|
||||
|
||||
Second, the effect of varying the mortality of a chain with a single
|
||||
generation.
|
||||
|
||||
======== ========= =========================
|
||||
Capacity Mortality Execution time (user+sys)
|
||||
======== ========= =========================
|
||||
6400 0.20 55.4
|
||||
6400 0.40 54.0
|
||||
6400 0.60 54.0
|
||||
6400 0.80 53.5
|
||||
6400 0.99 54.8
|
||||
======== ========= =========================
|
||||
|
||||
Third, the effect of varying the number of generations (all
|
||||
Second, the effect of varying the number of generations (all
|
||||
generations being identical).
|
||||
|
||||
=========== ======== ========= =========================
|
||||
Generations Capacity Mortality Execution time (user+sys)
|
||||
=========== ======== ========= =========================
|
||||
1 6400 0.80 53.5
|
||||
2 6400 0.80 42.4
|
||||
3 6400 0.80 42.1
|
||||
4 6400 0.80 42.2
|
||||
5 6400 0.80 42.2
|
||||
=========== ======== ========= =========================
|
||||
=========== ======== =========================
|
||||
Generations Capacity Execution time (user+sys)
|
||||
=========== ======== =========================
|
||||
1 6400 53.5
|
||||
2 6400 42.4
|
||||
3 6400 42.1
|
||||
4 6400 42.2
|
||||
5 6400 42.2
|
||||
=========== ======== =========================
|
||||
|
||||
These tables suggest that:
|
||||
|
||||
|
|
@ -80,10 +67,6 @@ These tables suggest that:
|
|||
sizes right is dramatic: much bigger than the small improvements to
|
||||
gained from other techniques.
|
||||
|
||||
#. The predicted mortality doesn't make much difference to the overall
|
||||
execution time (it does affect the distribution of pause times,
|
||||
however: see :ref:`topic-collection-schedule`.)
|
||||
|
||||
#. You can make generations too big as well as too small.
|
||||
|
||||
#. There are rapidly diminishing returns to be gained from adding
|
||||
|
|
@ -97,7 +80,7 @@ These tables suggest that:
|
|||
|
||||
The table below shows the effect of varying the initial allocation of
|
||||
address space to the arena (using three generations each with capacity
|
||||
6400 kB, mortality 0.80).
|
||||
6400 kB).
|
||||
|
||||
============= ========== =========== =========================
|
||||
Address space Extensions Collections Execution time (user+sys)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,22 @@ Release notes
|
|||
=============
|
||||
|
||||
|
||||
.. _release-notes-1.116:
|
||||
|
||||
Release 1.116.0
|
||||
---------------
|
||||
|
||||
|
||||
New features
|
||||
............
|
||||
|
||||
#. The MPS now measures the mortality of a :term:`generation` each
|
||||
time it is collected, and maintains a moving average. This means
|
||||
that it is no longer important to provide an accurate estimate of
|
||||
the mortality when creating a :term:`generation chain` by calling
|
||||
:c:func:`mps_chain_create`..
|
||||
|
||||
|
||||
.. _release-notes-1.115:
|
||||
|
||||
Release 1.115.0
|
||||
|
|
|
|||
|
|
@ -461,8 +461,8 @@ Arena properties
|
|||
When the pause time is short, the MPS needs to take more slices of
|
||||
time in order to make :term:`garbage collection` progress, and
|
||||
make more use of :term:`barriers (1)` to support
|
||||
:term:`incremental collection`. This increases time overheads,
|
||||
and especially operating system overheads.
|
||||
:term:`incremental garbage collection`. This increases time
|
||||
overheads, and especially operating system overheads.
|
||||
|
||||
The pause time may be set to zero, in which case the MPS returns
|
||||
as soon as it can, without regard for overall efficiency. This
|
||||
|
|
@ -476,7 +476,7 @@ Arena properties
|
|||
The pause time may be set to infinity, in which case the MPS
|
||||
completes all outstanding :term:`garbage collection` work before
|
||||
returning from an operation. The consequence is that the MPS will
|
||||
be able to save on the overheads due to :term:`incremental
|
||||
be able to save on the overheads due to :term:`incremental garbage
|
||||
collection`, leading to lower total time spent in collection. This
|
||||
value is suitable for non-interactive applications where total
|
||||
time is important.
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ you can choose which chain it should use by passing the
|
|||
|
||||
Create a generation chain by preparing an array of
|
||||
:c:type:`mps_gen_param_s` structures giving the *capacity* (in
|
||||
kilobytes) and *predicted mortality* (between 0 and 1) of each
|
||||
generation, and passing them to :c:func:`mps_chain_create`.
|
||||
kilobytes) and *initial predicted mortality* (between 0 and 1
|
||||
inclusive) of each generation, and passing them to
|
||||
:c:func:`mps_chain_create`.
|
||||
|
||||
When the *new size* of a generation exceeds its capacity, the MPS will
|
||||
be prepared to start collecting the chain to which the generation
|
||||
|
|
@ -95,17 +96,29 @@ For example::
|
|||
} mps_gen_param_s;
|
||||
|
||||
``mps_capacity`` is the capacity of the generation, in
|
||||
:term:`kilobytes`. When the size of the generation
|
||||
exceeds this, the MPS will be prepared to start collecting it.
|
||||
:term:`kilobytes`. When the size of the generation exceeds this,
|
||||
the MPS will be prepared to start collecting it.
|
||||
|
||||
``mps_mortality`` is the predicted mortality of the generation:
|
||||
the proportion (between 0 and 1) of blocks in the generation that
|
||||
are expected to be :term:`dead` when the generation is collected.
|
||||
.. note::
|
||||
|
||||
These numbers are hints to the MPS that it may use to make
|
||||
decisions about when and what to collect: nothing will go wrong
|
||||
(other than suboptimal performance) if you make poor
|
||||
choices. See :ref:`topic-collection-schedule`.
|
||||
The name *capacity* is somewhat misleading. When a generation
|
||||
reaches its capacity the MPS may not be able to collect it
|
||||
immediately (for example because some other generation is
|
||||
being collected), but this does not prevent allocation into
|
||||
the generation, and so the size of a generation will often
|
||||
exceed its capacity.
|
||||
|
||||
``mps_mortality`` is the initial predicted mortality of the
|
||||
generation: the proportion (between 0 and 1 inclusive) of bytes in
|
||||
the generation that are expected to be :term:`dead` when the
|
||||
generation is collected.
|
||||
|
||||
.. note::
|
||||
|
||||
This value is only used as an initial estimate. The MPS
|
||||
measures the mortality each time it collects the generation,
|
||||
and maintains a moving average. So it is not important to
|
||||
provide an accurate estimate here.
|
||||
|
||||
|
||||
.. c:function:: mps_res_t mps_chain_create(mps_chain_t *chain_o, mps_arena_t arena, size_t gen_count, mps_gen_param_s *gen_params)
|
||||
|
|
@ -187,28 +200,6 @@ survive collection get promoted to generation *g*\+1. If the last
|
|||
generation in the chain is collected, the survivors are promoted into
|
||||
an :term:`arena`\-wide "top" generation.
|
||||
|
||||
The predicted mortality is used to estimate how long the collection
|
||||
will take, and this is used in turn to decide how much work the
|
||||
collector will do each time it has an opportunity to do some work. The constraints here are:
|
||||
|
||||
#. The :term:`client program` might have specified a limit on the
|
||||
acceptable length of the pause if the work is being done inside
|
||||
:c:func:`mps_arena_step`.
|
||||
|
||||
#. The collector needs to keep up with the :term:`client program`:
|
||||
that is, it has to collect garbage at least as fast as the client
|
||||
is producing it, otherwise the amount of garbage will grow without
|
||||
bound.
|
||||
|
||||
With perfect prediction, the collector's work should be smoothly
|
||||
distributed, with a small maximum pause time. Getting the predicted
|
||||
mortality wrong leads to "lumpy" distribution of collection work with
|
||||
a longer maximum pause time. If the predicted mortality is too high,
|
||||
the collector will start out by taking small time slices and then find
|
||||
that it has to catch up later by taking larger time slices. If the
|
||||
predicted mortality is too low, the collector will take larger time
|
||||
slices up front and then find that it is idle later on.
|
||||
|
||||
|
||||
.. index::
|
||||
single: garbage collection; start message
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue