Merge pull request #1473 from wolfpld/slomp/pin-tracks

Add "pin" toggle to anchor threads and plots (and GPU queues)
This commit is contained in:
Bartosz Taudul
2026-10-12 17:06:44 +02:00
committed by GitHub
10 changed files with 130 additions and 31 deletions

1
NEWS
View File

@@ -5,6 +5,7 @@ here.
vx.xx.x (2026-xx-xx)
--------------------
- Ability to pin/anchor threads and plots to the viewport.
- API break: various lock-related "AfterUnlock" instrumentation methods were
renamed to "BeforeUnlock". Call sites need to be adjusted with regard to
the placement around the lock unlock calls, to match the name.

View File

@@ -4134,6 +4134,7 @@ Labels accompanied by the \faCaretDown{}~symbol can be collapsed out of the view
\begin{itemize}
\item \emph{\faEyeSlash{}~Hide} -- Hides the label along with the content associated to it. To make the label visible again, you must find it in the options menu (section~\ref{options}).
\item \emph{\faThumbtack{}~Pin} -- Pins the track so that it remains visible while scrolling the timeline view vertically. Pinned threads and GPU queues are anchored to the top of the view, while pinned plots are anchored to the bottom. Selecting \emph{Unpin} on a pinned track releases it.
\end{itemize}
Under the \faCaretDown{}~symbol are a series of points that allow to limit the depth of the zones displayed. Hover the~\faArrowPointer{}~mouse pointer over a circle to display a line visualizing the cutting point, then click the \MMB{}~middle mouse button to apply or remove a zone depth limit.

View File

@@ -41,7 +41,7 @@ void TimelineController::Begin()
m_items.clear();
}
void TimelineController::UpdateCenterItem()
void TimelineController::UpdateCenterItem( int pinnedTop )
{
ImVec2 mousePos = ImGui::GetMousePos();
@@ -53,16 +53,22 @@ void TimelineController::UpdateCenterItem()
const auto timelineMousePosY = mousePos.y - ImGui::GetWindowPos().y;
int centerY = timelineMousePosY + ImGui::GetScrollY();
// Pinned items are fixed to the viewport and excluded from the scrolling
// flow, so centering only considers the normal items in the middle band.
if( m_normalItems.empty() ) return;
const void* firstNormalKey = m_normalItems.front()->GetKey();
const void* lastNormalKey = m_normalItems.back()->GetKey();
int yBegin = 0;
int yEnd = 0;
for( auto& item : m_items )
int yEnd = pinnedTop;
for( auto& item : m_normalItems )
{
m_centerItemkey = item->GetKey();
yBegin = yEnd;
yEnd += item->GetHeight();
const auto inLowerBounds = m_centerItemkey == m_items.front()->GetKey() || yBegin <= centerY;
const auto inUpperBounds = m_centerItemkey == m_items.back()->GetKey() || centerY < yEnd;
const auto inLowerBounds = m_centerItemkey == firstNormalKey || yBegin <= centerY;
const auto inUpperBounds = m_centerItemkey == lastNormalKey || centerY < yEnd;
if( inLowerBounds && inUpperBounds )
{
@@ -72,7 +78,7 @@ void TimelineController::UpdateCenterItem()
}
}
std::optional<int> TimelineController::CalculateScrollPosition() const
std::optional<int> TimelineController::CalculateScrollPosition( int pinnedTop ) const
{
if( !m_centerItemkey ) return std::nullopt;
@@ -83,8 +89,8 @@ std::optional<int> TimelineController::CalculateScrollPosition() const
const auto timelineMousePosY = mousePos.y - ImGui::GetWindowPos().y;
int yBegin = 0;
int yEnd = 0;
for( auto& item : m_items )
int yEnd = pinnedTop;
for( auto& item : m_normalItems )
{
yBegin = yEnd;
yEnd += item->GetHeight();
@@ -96,11 +102,32 @@ std::optional<int> TimelineController::CalculateScrollPosition() const
return scrollY;
}
// A "just-pinned" center item is gone from the normal run but will be
// "re-picked" on next mouse move.
return std::nullopt;
}
void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool vcenter, float yMin, float yMax )
{
// Snapshot the pin classification once per frame before any Draw runs.
// Toggling a pin flips its state mid-frame and since re-checking it in
// the draw passes would draw the item twice (normal and band passes),
// the grouping here defers the change to the next frame.
// GetHeight() is 0 on the first frame, so the scroll extent would be shorter
// if a track was pinned at load, but self-corrects next frame (and nothing is
// pinned at load anyway).
m_normalItems.clear();
m_pinnedTopItems.clear();
m_pinnedBottomItems.clear();
int pinnedTop = 0;
int pinnedBottom = 0;
for( auto& item : m_items )
{
if( !item->IsPinned() ) m_normalItems.push_back( item );
else if( item->PinToBottom() ) { m_pinnedBottomItems.push_back( item ); pinnedBottom += item->GetHeight(); }
else { m_pinnedTopItems.push_back( item ); pinnedTop += item->GetHeight(); }
}
auto shouldUpdateCenterItem = [&] () {
const auto imguiChangedScroll = m_scroll != ImGui::GetScrollY();
const auto& mouseDelta = ImGui::GetIO().MouseDelta;
@@ -117,7 +144,7 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool
}
else if( shouldUpdateCenterItem() )
{
UpdateCenterItem();
UpdateCenterItem( pinnedTop );
}
const auto& viewData = m_view.GetViewData();
@@ -138,29 +165,79 @@ void TimelineController::End( double pxns, const ImVec2& wpos, bool hover, bool
ctx.wpos = wpos;
ctx.hover = hover;
int yOffset = 0;
const int curScrollY = (int)ImGui::GetScrollY();
const int windowHeight = (int)ImGui::GetWindowHeight();
// Keep the pinned bands from overlapping (top wins), otherwise bottom items
// would cover top ones while leaving them clickable underneath.
const int bottomStart = std::max( curScrollY + pinnedTop, curScrollY + windowHeight - pinnedBottom );
// Screen-space band edges, shared by culling, the mouse tests, and the fills.
const float topBandBegin = wpos.y + curScrollY;
const float topBandEnd = topBandBegin + pinnedTop;
const float bottomBandBegin = wpos.y + bottomStart;
const float bottomBandEnd = topBandBegin + windowHeight;
TimelineContext ctxNormal = ctx;
if( pinnedTop > 0 ) ctxNormal.yMin = std::max<float>( ctx.yMin, topBandEnd );
if( pinnedBottom > 0 ) ctxNormal.yMax = std::min<float>( ctx.yMax, bottomBandBegin );
const auto mouseY = ImGui::GetMousePos().y;
const bool mouseInTopBand = pinnedTop > 0 && mouseY >= topBandBegin && mouseY < topBandEnd;
const bool mouseInBottomBand = pinnedBottom > 0 && mouseY >= bottomBandBegin && mouseY < bottomBandEnd;
ctxNormal.hover = ctx.hover && !mouseInTopBand && !mouseInBottomBand;
// Preprocess runs before any Draw, so live pin state is stable here. Normal
// items are culled to the middle band and pinned items are always on screen.
int topOffset = 0, normalOffset = 0, bottomOffset = 0;
for( auto& item : m_items )
{
const bool pinned = item->IsPinned();
const bool toBottom = pinned && item->PinToBottom();
const int off = !pinned ? pinnedTop + normalOffset : toBottom ? bottomStart + bottomOffset : curScrollY + topOffset;
if( item->WantPreprocess() && item->IsVisible() )
{
const auto yPos = wpos.y + yOffset;
const bool visible = m_firstFrame || ( yPos < yMax && yPos + item->GetHeight() >= yMin );
const auto yPos = wpos.y + off;
const bool visible = pinned || m_firstFrame || ( yPos < ctxNormal.yMax && yPos + item->GetHeight() >= ctxNormal.yMin );
item->Preprocess( ctx, m_td, visible, yPos );
}
yOffset += m_firstFrame ? 0 : item->GetHeight();
const int h = m_firstFrame ? 0 : item->GetHeight();
if( !pinned ) normalOffset += h;
else if( toBottom ) bottomOffset += h;
else topOffset += h;
}
m_td.Sync();
yOffset = 0;
for( auto& item : m_items )
{
auto currentFrameItemHeight = item->GetHeight();
item->Draw( m_firstFrame, ctx, yOffset );
if( m_firstFrame ) currentFrameItemHeight = item->GetHeight();
yOffset += currentFrameItemHeight;
}
auto draw = ImGui::GetWindowDrawList();
if( const auto scrollY = CalculateScrollPosition() )
// Draws a group at (base + accumulated height) with respect to the
// first-frame height bootstrap, and returns the total height drawn.
auto drawRun = [&]( std::vector<TimelineItem*>& items, const TimelineContext& c, int base ) -> int {
int running = 0;
for( auto& item : items )
{
auto h = item->GetHeight();
item->Draw( m_firstFrame, c, base + running );
if( m_firstFrame ) h = item->GetHeight();
running += h;
}
return running;
};
// Clip (rather than paint over) so parent-window overlays stay visible inside the bands.
ImGui::PushClipRect( ImVec2( draw->GetClipRectMin().x, topBandEnd ), ImVec2( draw->GetClipRectMax().x, bottomBandBegin ), true );
const int normalRunning = drawRun( m_normalItems, ctxNormal, pinnedTop );
ImGui::PopClipRect();
const int topRunning = drawRun( m_pinnedTopItems, ctx, curScrollY );
const int bottomRunning = drawRun( m_pinnedBottomItems, ctx, bottomStart );
const auto pinBorderColor = 0xFF66FFFF;
if( topRunning > 0 ) draw->AddRect( ImVec2( wpos.x + 0.5f, topBandBegin + 0.5f ), ImVec2( wpos.x + ctx.w - 0.5f, topBandBegin + topRunning - 0.5f ), pinBorderColor );
if( bottomRunning > 0 ) draw->AddRect( ImVec2( wpos.x + 0.5f, bottomBandBegin + 0.5f ), ImVec2( wpos.x + ctx.w - 0.5f, bottomBandBegin + bottomRunning - 0.5f ), pinBorderColor );
int yOffset = pinnedTop + normalRunning + pinnedBottom;
// pinnedTop is a pre-Draw height, so vertical-centre compensation lags one frame
if( const auto scrollY = CalculateScrollPosition( pinnedTop ) )
{
int clampedScrollY = std::min<int>( *scrollY, std::max<int>( yOffset - ImGui::GetWindowHeight(), 0 ) );
ImGui::SetScrollY( clampedScrollY );

View File

@@ -46,10 +46,13 @@ public:
}
private:
void UpdateCenterItem();
std::optional<int> CalculateScrollPosition() const;
void UpdateCenterItem( int pinnedTop );
std::optional<int> CalculateScrollPosition( int pinnedTop ) const;
std::vector<TimelineItem*> m_items;
std::vector<TimelineItem*> m_normalItems;
std::vector<TimelineItem*> m_pinnedTopItems;
std::vector<TimelineItem*> m_pinnedBottomItems;
unordered_flat_map<const void*, std::unique_ptr<TimelineItem>> m_itemMap;
float m_height;

View File

@@ -12,6 +12,7 @@ namespace tracy
TimelineItem::TimelineItem( View& view, Worker& worker, const void* key, bool wantPreprocess )
: m_visible( true )
, m_showFull( true )
, m_pinned( false )
, m_height( 0 )
, m_wantPreprocess( wantPreprocess )
, m_key( key )
@@ -82,13 +83,15 @@ void TimelineItem::Draw( bool firstFrame, const TimelineContext& ctx, int yOffse
DrawTextContrast( draw, wpos + ImVec2( 0, hdrOffset ), colorInactive, ICON_FA_CARET_RIGHT );
}
const auto label = HeaderLabel();
labelWidth = ImGui::CalcTextSize( label ).x;
const auto pinWidth = IsPinned() ? ImGui::CalcTextSize( ICON_FA_THUMBTACK " " ).x : 0.f;
labelWidth = pinWidth + ImGui::CalcTextSize( label ).x;
if( m_showFull )
{
DrawLine( draw, dpos + ImVec2( 0, hdrOffset + ty - 1 ), dpos + ImVec2( w, hdrOffset + ty - 1 ), HeaderLineColor() );
HeaderExtraContents( ctx, hdrOffset, labelWidth );
}
DrawTextContrast( draw, wpos + ImVec2( ty, hdrOffset ), m_showFull ? color : colorInactive, label );
if( IsPinned() ) DrawTextContrast( draw, wpos + ImVec2( ty, hdrOffset ), m_showFull ? color : colorInactive, ICON_FA_THUMBTACK );
DrawTextContrast( draw, wpos + ImVec2( ty + pinWidth, hdrOffset ), m_showFull ? color : colorInactive, label );
if( ctx.hover && ImGui::IsMouseHoveringRect( wpos + ImVec2( 0, hdrOffset ), wpos + ImVec2( ty + labelWidth, hdrOffset + ty ) ) )
{
@@ -121,6 +124,11 @@ void TimelineItem::Draw( bool firstFrame, const TimelineContext& ctx, int yOffse
SetVisible( false );
ImGui::CloseCurrentPopup();
}
if( ImGui::MenuItem( IsPinned() ? ICON_FA_THUMBTACK_SLASH " Unpin" : ICON_FA_THUMBTACK " Pin" ) )
{
SetPinned( !IsPinned() );
ImGui::CloseCurrentPopup();
}
DrawExtraPopupItems();
ImGui::EndPopup();
}

View File

@@ -30,6 +30,12 @@ public:
virtual void SetVisible( bool visible ) { m_visible = visible; }
virtual bool IsVisible() const { return m_visible; }
void SetPinned( bool pinned ) { m_pinned = pinned; }
bool IsPinned() const { return m_pinned; }
// Pinned items are lifted out of the scrolling flow: false keeps them fixed
// at the top of the viewport (queues/threads), true at the bottom (plots).
virtual bool PinToBottom() const { return false; }
void SetShowFull( bool showFull ) { m_showFull = showFull; }
// returns 0 instead of the correct value for the first frame
@@ -58,6 +64,7 @@ protected:
bool m_visible;
bool m_showFull;
bool m_pinned;
private:
void AdjustThreadHeight( bool firstFrame, int yBegin, int yEnd );

View File

@@ -19,6 +19,8 @@ protected:
uint32_t HeaderLineColor() const override { return 0x8844DDDD; }
const char* HeaderLabel() const override;
bool PinToBottom() const override { return true; }
int64_t RangeBegin() const override;
int64_t RangeEnd() const override;

View File

@@ -324,7 +324,7 @@ private:
void DrawTimeline();
void DrawSampleList( const TimelineContext& ctx, const std::vector<SamplesDraw>& drawList, const Vector<SampleData>& vec, int offset, uint64_t tid );
void DrawZoneList( const TimelineContext& ctx, const std::vector<TimelineDraw>& drawList, int offset, uint64_t tid, int maxDepth, double margin );
void DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches );
void DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches, const bool hover );
void DrawContextSwitchList( const TimelineContext& ctx, const std::vector<ContextSwitchDraw>& drawList, const Vector<ContextSwitchData>& ctxSwitch, int offset, int endOffset, bool isFiber, uint64_t tid );
int DispatchGpuZoneLevel( const Vector<short_ptr<GpuEvent>>& vec, bool hover, double pxns, int64_t nspx, const ImVec2& wpos, int offset, int depth, uint64_t thread, float yMin, float yMax, int64_t begin, int drift );
template<typename Adapter, typename V>

View File

@@ -445,7 +445,7 @@ bool View::DrawCpuData( const TimelineContext& ctx, const std::vector<CpuUsageDr
offset += sstep;
}
if( ImGui::IsMouseHoveringRect( wpos, wpos + ImVec2( w, offset ) ) && IsMouseClickReleased( ImGuiMouseButton_Left ) )
if( hover && ImGui::IsMouseHoveringRect( wpos, wpos + ImVec2( w, offset ) ) && IsMouseClickReleased( ImGuiMouseButton_Left ) )
{
if( m_drawThreadHighlight != 0 )
{

View File

@@ -99,7 +99,7 @@ void View::DrawThread( const TimelineContext& ctx, const ThreadData& thread, con
if( displayCropper )
{
ImGui::PopClipRect();
if( depth > 0 ) DrawThreadCropper( depth, thread.id, croppperPosX, yPos, ostep, cropperWidth, hasCtxSwitch );
if( depth > 0 ) DrawThreadCropper( depth, thread.id, croppperPosX, yPos, ostep, cropperWidth, hasCtxSwitch, ctx.hover );
}
}
@@ -640,7 +640,7 @@ void View::DrawZoneList( const TimelineContext& ctx, const std::vector<TimelineD
}
}
void View::DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches )
void View::DrawThreadCropper( const int depth, const uint64_t tid, const float xPos, const float yPos, const float ostep, const float cropperWidth, const bool hasCtxSwitches, const bool hover )
{
const ImVec2 mousePos = ImGui::GetMousePos();
const bool clicked = IsMouseClicked( ImGuiMouseButton_Left );
@@ -677,7 +677,7 @@ void View::DrawThreadCropper( const int depth, const uint64_t tid, const float x
const float dx = mousePos.x - center.x;
const float dy = mousePos.y - center.y;
if( dx * dx + dy * dy <= hradius * hradius )
if( hover && dx * dx + dy * dy <= hradius * hradius )
{
draw->AddCircle( center, hradius, 0xFFFFFFFF, 0, hoverCircleThickness );
const float wPosX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMin().x;