Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a84437d
wpf-ar(iter=020, bench=geom-parser-isnumber-digit-first): reorder IsN…
May 6, 2026
294c099
wpf-ar(iter=023, bench=geom-parser-readnumber-sign-via-token): reuse …
May 6, 2026
81d37fd
wpf-ar(iter=026, bench=geom-parser-readnumber-simple-int-hoist-string…
May 6, 2026
192f2ee
wpf-ar(iter=027, bench=geom-parser-skipdigits-hoist-locals): hoist _p…
May 6, 2026
6d70c00
wpf-ar(iter=006, bench=geometry-skipws-hoist-locals): hoist _pathStri…
May 7, 2026
8d6b56e
wpf-ar(iter=025, bench=geometry-skipws-stash-token): stash the first …
May 8, 2026
09572a7
wpf-ar(iter=028, bench=cpec-inline-ccm-into-self): inline CultureAndC…
May 8, 2026
1e0dcaa
wpf-ar(iter=029, bench=cpec-threadstatic-pool): add a [ThreadStatic] …
May 8, 2026
a49810d
PresentationCore: reuse LayoutEventList.CopyToArray buffer
May 2, 2026
c335f1c
PresentationCore: split CopyToArray to keep GetAutomationRoots reentr…
May 3, 2026
6a3d8e9
wpf-ar(iter=026, bench=hwndwin32-syncctx-cache-legacy-only): cache Se…
May 8, 2026
541a112
wpf-ar(iter=032, bench=geometry-streamcontext-threadstatic-pool): cac…
May 8, 2026
3a72c77
wpf-ar(iter=033, bench=geometry-chunklist-singleitemlist-pool): pool …
May 8, 2026
158ed76
wpf-ar(iter=034, bench=geometry-abbreviated-parser-threadstatic-pool)…
May 8, 2026
14f979b
wpf-ar(iter=034, bench=geometry-skip-fillrule-default-setter): wrap `…
May 8, 2026
e6171b5
wpf-ar(iter=039, bench=cpec-skip-culture-setter-when-refequal): conve…
May 8, 2026
93aad70
wpf-ar(iter=045, bench=geometry-readwritedata-fits-in-chunk-fastpath)…
May 9, 2026
9c1cffe
wpf-ar(iter=047, bench=geometry-readnumber-singlepass-int-retry): col…
May 9, 2026
1e2f486
wpf-ar(iter=049, bench=geometry-readnumber-endchar-capture-fullhoist)…
May 9, 2026
b068cb5
wpf-ar(iter=050, bench=geometry-aggressive-inline-skipws-isnumber): m…
May 9, 2026
2c706ff
wpf-ar(iter=062, bench=excwrapper-no-handlers-fastpath-aggressive-inl…
May 9, 2026
ab66408
wpf-ar(iter=066, bench=cpec-skip-finally-restore-when-callback-untouc…
May 9, 2026
5da6eee
perf: add Visual.TryTransformToAncestorAsMatrix internal fast path
May 9, 2026
78e0299
perf: pool branchNodeStack via [ThreadStatic] in UIElementHelper
May 9, 2026
b4116e9
perf: pool removeList + keys snapshot in AdornerLayer.UpdateAdorner
May 9, 2026
82dc1d5
perf: dirty-bit guard around AdornerLayer.UpdateAdorner walk
May 9, 2026
bf3aade
perf: AdornerLayer uses Visual.TryTransformToAncestorAsMatrix fast path
May 9, 2026
74cea6a
wpf-ar(iter=074, bench=clock-computeevents-pool-activeperiod-tic): po…
May 10, 2026
dfe6bd4
perf: empty-AdornerLayer fast path in OnLayoutUpdated
May 10, 2026
8a1887a
wpf-ar(iter=082, bench=hwndwrapper-wndproc-isincreatewindow-hoist): h…
May 10, 2026
a8ae24f
wpf-ar(iter=086, bench=pushframeimpl-default-syncctx-reuse): reuse th…
May 11, 2026
47cf07f
wpf-ar(iter=087, bench=window-hwndstylemanager-per-window-pool): park…
May 11, 2026
cdde5f4
wpf-ar(iter=089, bench=window-showdialog-enumthreadwindows-delegate-c…
May 11, 2026
2191dae
wpf-ar(iter=090, bench=window-showdialog-threadwindowhandles-list-poo…
May 11, 2026
9de015d
wpf-ar(iter=093, bench=dispatcher-invokeimpl-priority-syncctx-cache):…
May 11, 2026
1a7d7ea
wpf-ar(iter=094, bench=priorityqueue-priorityitem-pool): pool Priorit…
May 11, 2026
49f1d2b
wpf-ar(iter=095, bench=dispatcheroperationevent-tls-pool): pool the c…
May 11, 2026
ae1f96c
wpf-ar(iter=097, bench=dispatcher-sync-invoke-action-no-asyncstate-ma…
May 11, 2026
45770ec
wpf-perf(big-win T2-A): [ThreadStatic]-pool ByteStreamGeometryContext…
May 11, 2026
7ad5001
wpf-perf(big-win T1-#1): pool UIElement.InputHitTest infrastructure
May 11, 2026
0a5dcf1
Reapply "wpf-perf(big-win T1-#2): use ThousandthOfEmRealPoints/RealDo…
May 11, 2026
7831813
wpf-perf(big-win T4): pool AdornerLayer._zOrderMap value snapshot
May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2951,6 +2951,7 @@ out nominalY
}
else
{
double emSizeReal = textFormatterImp.IdealToReal(lsrun.EmSize, currentLine.PixelsPerDip);
if (justify)
{
AdjustMetricsForDisplayModeJustifiedText(
Expand All @@ -2967,21 +2968,22 @@ out glyphAdvances
}
else
{
glyphAdvances = new List<double>(glyphCount);
glyphAdvances = new ThousandthOfEmRealDoubles(emSizeReal, glyphCount);
for (int i = 0; i < glyphCount; i++)
{
glyphAdvances.Add(textFormatterImp.IdealToReal(piJustifiedGlyphAdvances[i], currentLine.PixelsPerDip));
glyphAdvances[i] = textFormatterImp.IdealToReal(piJustifiedGlyphAdvances[i], currentLine.PixelsPerDip);
}
}
glyphOffsets = new List<Point>(glyphCount);
ThousandthOfEmRealPoints glyphOffsetsTyped = new ThousandthOfEmRealPoints(emSizeReal, glyphCount);
for (int i = 0; i < glyphCount; i++)
{
glyphIndices[i] = puGlyphs[i];
glyphOffsets.Add(new Point(
glyphOffsetsTyped[i] = new Point(
textFormatterImp.IdealToReal(piiGlyphOffsets[i].du, currentLine.PixelsPerDip),
textFormatterImp.IdealToReal(piiGlyphOffsets[i].dv, currentLine.PixelsPerDip)
));
);
}
glyphOffsets = glyphOffsetsTyped;
}

#if CHECK_GLYPHS
Expand Down Expand Up @@ -3104,11 +3106,14 @@ out charWidths
}
else
{
charWidths = new List<double>(cchText);
ThousandthOfEmRealDoubles charWidthsTyped = new ThousandthOfEmRealDoubles(
textFormatterImp.IdealToReal(lsrun.EmSize, Draw.CurrentLine.PixelsPerDip),
cchText);
for (int i = 0; i < cchText; i++)
{
charWidths.Add(textFormatterImp.IdealToReal(piCharAdvances[i], Draw.CurrentLine.PixelsPerDip));
charWidthsTyped[i] = textFormatterImp.IdealToReal(piCharAdvances[i], Draw.CurrentLine.PixelsPerDip);
}
charWidths = charWidthsTyped;
}
for (int i = 0; i < cchText; i++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ namespace MS.Internal
{
internal static class UIElementHelper
{
[ThreadStatic]
private static Stack<DependencyObject> _branchNodeStackCache;


internal static bool IsHitTestVisible(DependencyObject o)
{
Debug.Assert(o != null, "UIElementHelper.IsHitTestVisible called with null argument");
Expand Down Expand Up @@ -138,7 +142,8 @@ internal static void InvalidateAutomationAncestors(DependencyObject o)
UIElement3D e3d = null;
ContentElement ce = null;

Stack<DependencyObject> branchNodeStack = new Stack<DependencyObject>();
var branchNodeStack = _branchNodeStackCache ??= new Stack<DependencyObject>();
branchNodeStack.Clear(); // defensive: guard against unexpected residue from any prior walk
bool continueInvalidation = true;

while (o != null && continueInvalidation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,9 +575,9 @@ private void fireLayoutUpdateEvent()
{
_inFireLayoutUpdated = true;

LayoutEventList.ListItem [] copy = LayoutEvents.CopyToArray();
LayoutEventList.ListItem [] copy = LayoutEvents.CopyToReusableArray(out int copyCount);

for(int i=0; i<copy.Length; i++)
for(int i=0; i<copyCount; i++)
{
LayoutEventList.ListItem item = copy[i];
//store handler here in case if thread gets pre-empted between check for IsAlive and invocation
Expand Down Expand Up @@ -678,9 +678,9 @@ private void fireAutomationEvents()
_inFireAutomationEvents = true;
_firePostLayoutEvents = false;

LayoutEventList.ListItem [] copy = AutomationEvents.CopyToArray();
LayoutEventList.ListItem [] copy = AutomationEvents.CopyToReusableArray(out int copyCount);

for(int i=0; i<copy.Length; i++)
for(int i=0; i<copyCount; i++)
{
LayoutEventList.ListItem item = copy[i];
//store peer here in case if thread gets pre-empted between check for IsAlive and invocation
Expand Down Expand Up @@ -729,6 +729,12 @@ internal LayoutEventList AutomationEvents

internal AutomationPeer[] GetAutomationRoots()
{
// Use the parameterless CopyToArray (fresh snapshot) here, NOT
// CopyToReusableArray. This method is reachable from inside
// fireAutomationEvents → peer.FireAutomationEvents() →
// AutomationPeer.ValidateConnected (LayoutManager.cs callers in
// AutomationPeer.cs:578), which would otherwise overwrite the
// shared _copyBuffer mid-iteration of the outer fire loop.
LayoutEventList.ListItem [] copy = AutomationEvents.CopyToArray();

AutomationPeer[] peers = new AutomationPeer[copy.Length];
Expand Down Expand Up @@ -1127,6 +1133,57 @@ internal ListItem[] CopyToArray()
return copy;
}

// Per-instance scratch buffer for CopyToReusableArray. Each call to
// fireLayoutUpdateEvent / fireAutomationEvents (one per render pass
// when there is layout dirtiness) used to allocate a fresh
// ListItem[_count]; with the typical hundreds of UIElements that
// subscribe to LayoutUpdated that's hundreds of MB of gen0 churn
// during sustained playback. The buffer is shared across calls, so
// it is only safe to use from a caller that can guarantee no other
// CopyToReusableArray call on the same LayoutEventList runs before
// iteration over the returned buffer is complete. Reentrant or
// on-demand callers (e.g. GetAutomationRoots, which can be reached
// from inside FireAutomationEvents handlers via AutomationPeer
// connectivity checks) must use the parameterless CopyToArray()
// instead so they get a private snapshot.
//
// Caller must use the returned `count`; buffer length may exceed it.
private ListItem[] _copyBuffer;

internal ListItem[] CopyToReusableArray(out int count)
{
count = _count;
ListItem[] buffer = _copyBuffer;
if (buffer == null || buffer.Length < count)
{
// Round up to next power of two so steady-state subscriber
// counts don't keep reallocating on small swings.
int newSize = buffer == null ? 16 : buffer.Length;
while (newSize < count) newSize *= 2;
buffer = new ListItem[newSize];
_copyBuffer = buffer;
}

ListItem t = _head;
int i = 0;
while (t != null)
{
buffer[i++] = t;
t = t.Next;
}
// Clear the tail of any stale references from a previous fire so
// ListItems removed during/after that fire can be GC'd. Removed
// ListItems already null their Target (see reuseListItem), so
// this only retains lightweight WeakReference shells, but we
// null them anyway to keep the invariant "buffer beyond `count`
// is null" so callers can't mistakenly read stale entries.
for (int j = i; j < buffer.Length && buffer[j] != null; j++)
{
buffer[j] = null;
}
return buffer;
}

internal int Count
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2581,10 +2581,16 @@ private void ComputeEvents(TimeSpan? expirationTime,

// consider caching this condition
// We check whether our active period exists before using it to compute intervals
if (!expirationTime.HasValue // If activePeriod extends forever,
|| expirationTime >= _beginTime) // OR if activePeriod extends to or beyond _beginTime,
if (!expirationTime.HasValue // If activePeriod extends forever,
|| expirationTime >= _beginTime) // OR if activePeriod extends to or beyond _beginTime,
{
// Check for CurrentTimeInvalidated
// The activePeriod TIC was previously freshly allocated per tick (3 small arrays via
// CreateClosedOpenInterval / CreateInfiniteClosedInterval), but it is only used for two
// read-only Intersects calls (one here on the Intersects, and one inside
// ComputeIntervalsWithParentIntersection on IntersectsInverseOf) and never escapes the
// call stack. We rebuild it in place on a per-thread scratch buffer to eliminate the
// per-tick array allocations on every animated clock.
TimeIntervalCollection activePeriod;
if (expirationTime.HasValue)
{
Expand All @@ -2594,12 +2600,14 @@ private void ComputeEvents(TimeSpan? expirationTime,
}
else
{
activePeriod = TimeIntervalCollection.CreateClosedOpenInterval(_beginTime.Value, expirationTime.Value);
s_scratchActivePeriod.RebuildAsClosedOpenInterval(_beginTime.Value, expirationTime.Value);
activePeriod = s_scratchActivePeriod;
}
}
else // expirationTime is infinity
{
activePeriod = TimeIntervalCollection.CreateInfiniteClosedInterval(_beginTime.Value);
s_scratchActivePeriod.RebuildAsInfiniteClosedInterval(_beginTime.Value);
activePeriod = s_scratchActivePeriod;
}

// If we have an intersection between parent domain times and the interval over which we
Expand Down Expand Up @@ -2797,7 +2805,11 @@ private void ComputeIntervalsWithHoldEnd(
{
Debug.Assert(endOfActivePeriod.HasValue);

TimeIntervalCollection fillPeriod = TimeIntervalCollection.CreateInfiniteClosedInterval(endOfActivePeriod.Value);
// Reuse the per-thread scratch buffer here too; this path is mutually exclusive with the
// activePeriod path in ComputeEvents (the caller takes the Intersects-true OR Intersects-false
// branch, not both), so a single scratch slot suffices for both fillPeriod and activePeriod.
s_scratchActivePeriod.RebuildAsInfiniteClosedInterval(endOfActivePeriod.Value);
TimeIntervalCollection fillPeriod = s_scratchActivePeriod;

if (parentIntervalCollection.Intersects(fillPeriod)) // We enter or leave Fill period
{
Expand Down Expand Up @@ -4469,6 +4481,17 @@ internal static void CleanKnownClocksTable()

private static Int64 s_TimeSpanTicksPerSecond = TimeSpan.FromSeconds(1).Ticks;

// Per-thread scratch TimeIntervalCollection used by ComputeEvents / ComputeIntervalsWithHoldEnd
// to avoid the per-tick allocation of three small arrays for activePeriod / fillPeriod. The
// struct's _nodeTime / _nodeIsPoint / _nodeIsInterval buffers are allocated on first use and
// reused across every Clock.ComputeEvents call on the dispatcher thread thereafter. Both
// consumers (parentIntervalCollection.Intersects(activePeriod) and
// parentIntervalCollection.IntersectsInverseOf(activePeriod)) read this struct without mutating
// its underlying arrays, and ComputeEvents never recurses into another Clock's ComputeEvents
// before its own consumer calls return, so a single shared scratch slot is safe.
[ThreadStatic]
private static TimeIntervalCollection s_scratchActivePeriod;

#endregion // Linking data

#region Debug data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,64 @@ internal static TimeIntervalCollection CreateInfiniteClosedInterval(TimeSpan fro
return new TimeIntervalCollection(from, true);
}

// Rebuilds this TIC in place as the closed-open interval [from, to). Reuses the existing
// _nodeTime / _nodeIsPoint / _nodeIsInterval buffers (allocates only on first call when
// they are null). Mirrors the semantics of CreateClosedOpenInterval(from, to) exactly,
// including the from==to single-point degenerate case and the from>to swap.
internal void RebuildAsClosedOpenInterval(TimeSpan from, TimeSpan to)
{
_containsNullPoint = false;
_invertCollection = false;
_current = 0;

EnsureAllocatedCapacity(_minimumCapacity);

_nodeTime[0] = from;

if (from == to)
{
// Match TimeIntervalCollection(from,true,to,false) for from==to: single point at from.
_nodeIsPoint[0] = true;
_nodeIsInterval[0] = false;
_count = 1;
}
else if (from < to)
{
_nodeIsPoint[0] = true; // includeFrom
_nodeIsInterval[0] = true;
_nodeTime[1] = to;
_nodeIsPoint[1] = false; // !includeTo
_nodeIsInterval[1] = false; // explicit reset (constructor relied on fresh-array default)
_count = 2;
}
else // from > to: reversed, swap to [to, from) shape
{
_nodeTime[0] = to;
_nodeIsPoint[0] = false; // !includeTo
_nodeIsInterval[0] = true;
_nodeTime[1] = from;
_nodeIsPoint[1] = true; // includeFrom
_nodeIsInterval[1] = false; // explicit reset
_count = 2;
}
}

// Rebuilds this TIC in place as the half-infinite closed interval [from, +infinity).
// Reuses existing buffers (allocates only on first call). Mirrors CreateInfiniteClosedInterval(from).
internal void RebuildAsInfiniteClosedInterval(TimeSpan from)
{
_containsNullPoint = false;
_invertCollection = false;
_current = 0;

EnsureAllocatedCapacity(_minimumCapacity);

_nodeTime[0] = from;
_nodeIsPoint[0] = true; // includePoint
_nodeIsInterval[0] = true;
_count = 1;
}

/// <summary>
/// Creates an empty collection
/// </summary>
Expand Down
Loading
Loading