// Copyright Epic Games, Inc. All Rights Reserved. #include "UbaVisualizer.h" #include "UbaConfig.h" #include #include #include #include #pragma comment (lib, "Shcore.lib") #pragma comment (lib, "UxTheme.lib") #pragma comment (lib, "Dwmapi.lib") #define WM_NEWTRACE WM_USER+1 #define WM_SETTITLE WM_USER+2 namespace uba { enum { Popup_CopySessionInfo = 3, Popup_CopyProcessInfo, Popup_CopyProcessLog, Popup_CopyProcessBreadcrumbs, Popup_CopyWorkInfo, Popup_Replay, Popup_Pause, Popup_Play, Popup_JumpToEnd, #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) Popup_##name, UBA_VISUALIZER_FLAGS2 #undef UBA_VISUALIZER_FLAG Popup_IncreaseFontSize, Popup_DecreaseFontSize, Popup_SaveAs, Popup_SaveSettings, Popup_OpenSettings, Popup_Quit, }; VisualizerConfig::VisualizerConfig(const tchar* fn) : filename(fn) { fontName = TC("Arial"); } bool VisualizerConfig::Load(Logger& logger) { Config config; if (!config.LoadFromFile(logger, filename.c_str())) { DWORD value = 1; DWORD valueSize = sizeof(value); if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, NULL, &value, &valueSize) == ERROR_SUCCESS) DarkMode = value == 0; return false; } config.GetValueAsInt(x, L"X"); config.GetValueAsInt(y, L"Y"); config.GetValueAsU32(width, L"Width"); config.GetValueAsU32(height, L"Height"); config.GetValueAsU32(fontSize, L"FontSize"); config.GetValueAsString(fontName, L"FontName"); config.GetValueAsU32(maxActiveVisible, L"MaxActiveVisible"); config.GetValueAsU32(maxActiveProcessHeight, L"MaxActiveProcessHeight"); #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) config.GetValueAsBool(show##name, L"Show" #name); UBA_VISUALIZER_FLAGS1 #undef UBA_VISUALIZER_FLAG #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) config.GetValueAsBool(name, TC(#name)); UBA_VISUALIZER_FLAGS2 #undef UBA_VISUALIZER_FLAG fontSize = Min(fontSize, 30u); return true; } bool VisualizerConfig::Save(Logger& logger) { Config config; config.AddValue(L"X", x); config.AddValue(L"Y", y); config.AddValue(L"Width", width); config.AddValue(L"Height", height); config.AddValue(L"FontSize", fontSize); config.AddValue(L"FontName", fontName.c_str()); config.AddValue(L"MaxActiveVisible", maxActiveVisible); config.AddValue(L"MaxActiveProcessHeight", maxActiveProcessHeight); #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) config.AddValue(L"Show" #name, show##name); UBA_VISUALIZER_FLAGS1 #undef UBA_VISUALIZER_FLAG #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) config.AddValue(TC(#name), name); UBA_VISUALIZER_FLAGS2 #undef UBA_VISUALIZER_FLAG return config.SaveToFile(logger, filename.c_str()); } enum VisualizerFlag { #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) VisualizerFlag_##name, UBA_VISUALIZER_FLAGS1 #undef UBA_VISUALIZER_FLAG VisualizerFlag_Count }; class DrawTextLogger : public Logger { public: DrawTextLogger(HWND hw, HDC h, int fh, HBRUSH bb) : hwnd(hw) , hdc(h) , fontHeight(fh) , backgroundBrush(bb) { textColor = GetTextColor(hdc); } virtual void BeginScope() override {} virtual void EndScope() override {} virtual void Log(LogEntryType type, const wchar_t* str, u32 strLen) override { RECT textRect{0,0,0,0}; DrawTextW(hdc, str, strLen, &textRect, DT_CALCRECT); lines.emplace_back(TString(str, strLen), textOffset, height, textColor); width = Max(width, int(textRect.right + textOffset)); height += fontHeight; } void AddSpace(int space = 5) { height += space; } void AddTextOffset(int offset) { textOffset += offset; } void AddWidth(int extra) { extraWidth += extra; } void DrawAtPos(int x, int y) { RECT r; r.left = x; r.top = y; r.right = r.left + width; r.bottom = r.top + height; RECT clientRect; GetClientRect(hwnd, &clientRect); if (r.right > clientRect.right) OffsetRect(&r, -width - 15, 0); if (r.bottom > clientRect.bottom) { OffsetRect(&r, 0, clientRect.bottom - r.bottom); if (r.top < 0) OffsetRect(&r, 0, -r.top); } RECT fillRect = r; fillRect.right += 2 + extraWidth; FillRect(hdc, &fillRect, backgroundBrush); for (auto& line : lines) { RECT tr = r; tr.left += line.left; tr.top += line.top; SetTextColor(hdc, line.color); DrawTextW(hdc, line.str.data(), u32(line.str.size()), &tr, DT_SINGLELINE|DT_NOPREFIX); } } void DrawAtCursor() { POINT p; GetCursorPos(&p); ScreenToClient(hwnd, &p); p.x += 3; p.y += 3; DrawAtPos(p.x, p.y); } DrawTextLogger& SetColor(COLORREF c) { textColor = c; return *this; } int width = 0; int height = 0; int textOffset = 2; int extraWidth = 0; struct Line { TString str; int left; int top; COLORREF color; }; Vector lines; HWND hwnd; HDC hdc; int fontHeight; HBRUSH backgroundBrush; COLORREF textColor; bool isFirst = true; using Logger::Log; }; class WriteTextLogger : public Logger { public: WriteTextLogger(TString& out) : m_out(out) {} virtual void BeginScope() override {} virtual void EndScope() override {} virtual void Log(LogEntryType type, const wchar_t* str, u32 strLen) override { m_out.append(str, strLen).append(TC("\n")); } TString& m_out; }; Visualizer::Visualizer(VisualizerConfig& config, Logger& logger) : m_logger(logger) , m_config(config) , m_trace(logger) { memset(m_activeProcessFont, 0, sizeof(m_activeProcessFont)); memset(m_activeProcessCountHistory, 0, sizeof(m_activeProcessCountHistory)); } Visualizer::~Visualizer() { m_looping = false; // Make sure GetMessage is triggered out of its slumber PostMessage(m_hwnd, WM_QUIT, 0, 0); m_thread.Wait(); delete m_client; } bool Visualizer::ShowUsingListener(const wchar_t* channelName) { TraceChannel channel(m_logger); if (!channel.Init(channelName)) { m_logger.Error(L"TODO"); return false; } m_listenTimeout.Create(false); m_listenChannel.Append(channelName); m_looping = true; m_autoScroll = false; if (!StartHwndThread()) return true; { StringBuffer<> title; PostNewTitle(GetTitlePrefix(title).Appendf(L"Listening for new sessions on channel '%s'", m_listenChannel.data)); } StringBuffer<256> traceName; while (m_hwnd) { if (m_locked) { m_listenTimeout.IsSet(1000); continue; } if (m_parentHwnd && !IsWindow(m_parentHwnd)) PostQuit(); traceName.Clear(); if (!channel.Read(traceName)) { m_logger.Error(L"TODO2"); return false; } if (traceName.count) { StringBuffer<128> filter; if (!m_config.ShowAllTraces) { OwnerInfo ownerInfo = GetOwnerInfo(); if (ownerInfo.pid) filter.Appendf(L"_%s%u", ownerInfo.id, ownerInfo.pid); } if (!traceName.Equals(m_newTraceName.data) && traceName.EndsWith(filter)) { m_newTraceName.Clear().Append(traceName); m_usingNamed = true; PostNewTrace(0, false); } } else m_newTraceName.Clear(); m_listenTimeout.IsSet(1000); } return true; } bool Visualizer::ShowUsingNamedTrace(const wchar_t* namedTrace) { m_looping = true; if (!StartHwndThread()) return true; m_newTraceName.Append(namedTrace); m_usingNamed = true; PostNewTrace(0, false); return true; } bool Visualizer::ShowUsingSocket(NetworkBackend& backend, const wchar_t* host, u16 port) { auto destroyClient = MakeGuard([&]() { delete m_client; m_client = nullptr; }); m_looping = true; m_autoScroll = false; if (!StartHwndThread()) return true; m_clientDisconnect.Create(true); wchar_t dots[] = TC("...."); u32 dotsCounter = 0; StringBuffer<256> traceName; while (m_hwnd) { if (!m_client) { bool ctorSuccess = true; NetworkClientCreateInfo ncci; ncci.workerCount = 0; m_client = new NetworkClient(ctorSuccess, ncci); if (!ctorSuccess) return false; } StringBuffer<> title; PostNewTitle(GetTitlePrefix(title).Appendf(L"Trying to connect to %s:%u%s", host, port, dots + ((dotsCounter--) % 4))); bool timedOut; if (!m_client->Connect(backend, host, port, &timedOut)) continue; PostNewTitle(GetTitlePrefix(title).Appendf(L"Connected to %s:%u", host, port)); PostNewTrace(0, false); while (m_hwnd && m_client->IsConnected() && !m_clientDisconnect.IsSet(100)) { m_trace.UpdateReceiveClient(*m_client); } PostNewTitle(GetTitlePrefix(title).Appendf(L"Disconnected...")); m_client->Disconnect(); delete m_client; m_client = nullptr; m_clientDisconnect.Reset(); Sleep(4000); // To prevent it from reconnecting to the same thing again and get thrown out (since it will post a WM_NEWTRACE and clean everything } return true; } bool Visualizer::ShowUsingFile(const wchar_t* fileName, u32 replay) { m_looping = true; m_autoScroll = false; m_fileName.Append(fileName); if (!StartHwndThread()) return true; PostNewTrace(replay, 0); return true; } bool Visualizer::StartHwndThread() { m_thread.Start([this]() { ThreadLoop(); return 0;}, TC("UbaHwnd")); while (!m_hwnd) if (m_thread.Wait(10)) return false; return true; } bool Visualizer::HasWindow() { return m_looping == true; } HWND Visualizer::GetHwnd() { return m_hwnd; } void Visualizer::Lock(bool lock) { m_locked = lock; } StringBufferBase& Visualizer::GetTitlePrefix(StringBufferBase& out) { out.Clear(); out.Append(L"UbaVisualizer"); #if UBA_DEBUG out.Append(L" (DEBUG)"); #endif out.Append(L" - "); return out; } bool Visualizer::Unselect() { if (m_processSelected || m_sessionSelectedIndex != ~0u || m_statsSelected || m_timelineSelected != 0 || m_fetchedFilesSelected != ~0u || m_workSelected || !m_hyperLinkSelected.empty()) { m_processSelected = false; m_sessionSelectedIndex = ~0u; m_statsSelected = false; m_activeProcessGraphSelected = false; m_buttonSelected = ~0u; m_timelineSelected = 0; m_fetchedFilesSelected = ~0u; m_workSelected = false; m_hyperLinkSelected.clear(); return true; } return false; } void Visualizer::Reset() { for (HBITMAP bm : m_textBitmaps) DeleteObject(bm); DeleteObject(m_lastBitmap); m_contentWidth = 0; m_contentHeight = 0; m_textBitmaps.clear(); m_lastBitmap = 0; m_lastBitmapOffset = BitmapCacheHeight; //m_autoScroll = true; //m_scrollPosX = 0; //m_scrollPosY = 0; //m_zoomValue = 0.75f; //m_horizontalScaleValue = 1.0f; m_startTime = GetTime(); m_pauseTime = 0; Unselect(); } void Visualizer::InitBrushes() { if (m_config.DarkMode) { m_textColor = RGB(190, 190, 190); m_textWarningColor = RGB(190, 190, 0); m_textErrorColor = RGB(190, 0, 0); m_processBrushes[0].inProgress = CreateSolidBrush(RGB(70, 70, 70)); m_processBrushes[1].inProgress = CreateSolidBrush(RGB(130, 130, 130)); m_processBrushes[0].error = CreateSolidBrush(RGB(140, 0, 0)); m_processBrushes[1].error = CreateSolidBrush(RGB(190, 0, 0)); m_processBrushes[0].returned = CreateSolidBrush(RGB(50, 50, 120)); m_processBrushes[1].returned = CreateSolidBrush(RGB(70, 70, 160)); m_processBrushes[0].recv = CreateSolidBrush(RGB(10, 92, 10)); m_processBrushes[1].recv = CreateSolidBrush(RGB(10, 130, 10)); m_processBrushes[0].success = CreateSolidBrush(RGB(10, 100, 10)); m_processBrushes[1].success = CreateSolidBrush(RGB(10, 140, 10)); m_processBrushes[0].send = CreateSolidBrush(RGB(10, 115, 10)); m_processBrushes[1].send = CreateSolidBrush(RGB(10, 145, 10)); m_processBrushes[0].cacheFetch = CreateSolidBrush(RGB(24, 112, 110)); m_processBrushes[1].cacheFetch = CreateSolidBrush(RGB(31, 143, 138)); m_backgroundBrush = CreateSolidBrush(0x00252526); m_separatorPen = CreatePen(PS_SOLID, 1, RGB(50, 50, 50)); m_tooltipBackgroundBrush = CreateSolidBrush(0x00404040); m_checkboxPen = CreatePen(PS_SOLID, 1, RGB(130, 130, 130)); m_sendColor = RGB(0, 170, 0); m_recvColor = RGB(0, 170, 255); m_cpuColor = RGB(170, 170, 0); m_memColor = RGB(170, 0, 255); m_driveColor = RGB(170, 65, 55); m_activeProcColor = RGB(0, 170, 170); } else { m_textColor = GetSysColor(COLOR_INFOTEXT); m_textWarningColor = RGB(170, 130, 0); m_textErrorColor = RGB(190, 0, 0); m_processBrushes[0].inProgress = CreateSolidBrush(RGB(150, 150, 150)); m_processBrushes[1].inProgress = CreateSolidBrush(RGB(180, 180, 180)); m_processBrushes[0].error = CreateSolidBrush(RGB(255, 70, 70)); m_processBrushes[1].error = CreateSolidBrush(RGB(255, 100, 70)); m_processBrushes[0].returned = CreateSolidBrush(RGB(150, 150, 200)); m_processBrushes[1].returned = CreateSolidBrush(RGB(170, 170, 200)); m_processBrushes[0].recv = CreateSolidBrush(RGB(10, 190, 10)); m_processBrushes[1].recv = CreateSolidBrush(RGB(20, 210, 20)); m_processBrushes[0].success = CreateSolidBrush(RGB(10, 200, 10)); m_processBrushes[1].success = CreateSolidBrush(RGB(20, 220, 20)); m_processBrushes[0].send = CreateSolidBrush(RGB(80, 210, 80)); m_processBrushes[1].send = CreateSolidBrush(RGB(90, 250, 90)); m_processBrushes[0].cacheFetch = CreateSolidBrush(RGB(150, 150, 200)); m_processBrushes[1].cacheFetch = CreateSolidBrush(RGB(170, 170, 200)); m_backgroundBrush = GetSysColorBrush(0); m_separatorPen = CreatePen(PS_SOLID, 1, RGB(180, 180, 180)); m_tooltipBackgroundBrush = GetSysColorBrush(COLOR_INFOBK); m_checkboxPen = CreatePen(PS_SOLID, 1, RGB(130, 130, 130)); m_sendColor = RGB(0, 170, 0); // Green m_recvColor = RGB(63, 72, 204); // Blue m_cpuColor = RGB(200, 130, 0); // Orange m_memColor = RGB(170, 0, 255); // Purple m_driveColor = RGB(255, 115, 96); m_activeProcColor = RGB(0, 170, 170); } m_textPen = CreatePen(PS_SOLID, 1, m_textColor); m_sendPen = CreatePen(PS_SOLID, 1, m_sendColor); m_recvPen = CreatePen(PS_SOLID, 1, m_recvColor); m_cpuPen = CreatePen(PS_SOLID, 1, m_cpuColor); m_memPen = CreatePen(PS_SOLID, 1, m_memColor); m_drivePen = CreatePen(PS_SOLID, 1, m_driveColor); m_activeProcPen = CreatePen(PS_SOLID, 1, m_activeProcColor); } void Visualizer::ThreadLoop() { if (m_config.parent) { SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE); } InitBrushes(); LOGBRUSH br = { 0 }; GetObject(m_backgroundBrush, sizeof(br), &br); m_processUpdatePen = CreatePen(PS_SOLID, 2, RGB(GetRValue(br.lbColor), GetGValue(br.lbColor), GetBValue(br.lbColor))); HINSTANCE hInstance = GetModuleHandle(NULL); int winPosX = m_config.x; int winPosY = m_config.y; int winWidth = m_config.width; int winHeight = m_config.height; RECT rectCombined; SetRectEmpty(&rectCombined); EnumDisplayMonitors(0, 0, [](HMONITOR hMon,HDC hdc,LPRECT lprcMonitor,LPARAM pData) { RECT* rectCombined = reinterpret_cast(pData); UnionRect(rectCombined, rectCombined, lprcMonitor); return TRUE; }, (LPARAM)&rectCombined); winPosX = Max(int(rectCombined.left), winPosX); winPosY = Max(int(rectCombined.top), winPosY); winPosX = Min(int(rectCombined.right) - winWidth, winPosX); winPosY = Min(int(rectCombined.bottom) - winHeight, winPosY); WNDCLASSEX wndClassEx; ZeroMemory(&wndClassEx, sizeof(wndClassEx)); wndClassEx.cbSize = sizeof(wndClassEx); wndClassEx.style = CS_HREDRAW | CS_VREDRAW; wndClassEx.lpfnWndProc = &StaticWinProc; wndClassEx.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(123)); wndClassEx.hCursor = 0; wndClassEx.hInstance = hInstance; wndClassEx.hbrBackground = NULL; wndClassEx.lpszClassName = TEXT("UbaVisualizer"); ATOM wndClassAtom = RegisterClassEx(&wndClassEx); const TCHAR* windowClassName = MAKEINTATOM(wndClassAtom); auto unreg = MakeGuard([&]() { UnregisterClass(windowClassName, hInstance); }); UpdateDefaultFont(); UpdateProcessFont(); const TCHAR* fontName = TEXT("Consolas"); m_popupFont.handle = CreateFontW(-12, 0, 0, 0, FW_NORMAL, false, false, false, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, FIXED_PITCH | FF_MODERN, fontName); m_popupFont.height = 14; //m_popupFont = (HFONT)GetStockObject(SYSTEM_FIXED_FONT); DWORD scrollbarFlags = 0; m_verticalScrollBarEnabled = !ActiveProcessesShouldFillHeight(); if (m_verticalScrollBarEnabled) scrollbarFlags |= WS_VSCROLL; m_horizontalScrollBarEnabled = !m_config.AutoScaleHorizontal; if (m_horizontalScrollBarEnabled) scrollbarFlags |= WS_HSCROLL; DWORD windowStyle = WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CLIPCHILDREN | scrollbarFlags; DWORD exStyle = 0; if (m_config.parent) windowStyle = WS_POPUP | scrollbarFlags; StringBuffer<> title; GetTitlePrefix(title).Append(L"Initializing..."); HWND hwnd = CreateWindowEx(exStyle, windowClassName, title.data, windowStyle, winPosX, winPosY, winWidth, winHeight, NULL, NULL, hInstance, this); SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)this); m_hwnd = hwnd; auto destroyWin = [&]() { if (m_hwnd) { if (m_config.AutoSaveSettings) SaveSettings(); DestroyWindow(m_hwnd); m_hwnd = 0; } }; BOOL cloak = TRUE; DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak)); auto exitCloak = MakeGuard([&]() { cloak = FALSE; DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak)); }); if (m_config.DarkMode) UpdateTheme(); HitTestResult res; HitTest(res, { -1, -1 }); if (m_config.parent) { exitCloak.Execute(); // If not child it will not propagate keyboard presses etc to parent SetWindowLong(hwnd, GWL_STYLE, WS_CHILD | scrollbarFlags); m_parentHwnd = (HWND)(uintptr_t)m_config.parent; if (!SetParent(hwnd, m_parentHwnd)) m_logger.Error(L"SetParent failed using parentHwnd 0x%llx", m_parentHwnd); UpdateWindow(m_hwnd); UpdateScrollbars(true); PostMessage(m_parentHwnd, 0x0444, 0, (LPARAM)hwnd); } else { ShowWindow(hwnd, SW_SHOW); UpdateWindow(m_hwnd); UpdateScrollbars(true); exitCloak.Execute(); } m_startTime = GetTime(); while (m_looping) { DWORD timeoutMs = 2000; DWORD result = MsgWaitForMultipleObjects(0, NULL, FALSE, timeoutMs, QS_ALLINPUT); if (result == WAIT_TIMEOUT) continue; if (result != WAIT_OBJECT_0) break; MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); // It may happen that we receive the WM_DESTOY message from within the DistpachMessage above and handle it directly in WndProc. // So before trying to call GetMessage again, we just need to validate that m_looping is still true otherwise we could end // up waiting forever for this thread to exit. if (!m_looping || msg.message == WM_QUIT || msg.message == WM_DESTROY || msg.message == WM_CLOSE) { destroyWin(); m_looping = false; if (m_listenTimeout.IsCreated()) m_listenTimeout.Set(); break; } } } destroyWin(); } void Visualizer::Pause(bool pause) { if (m_paused == pause) return; m_paused = pause; if (pause) { m_pauseStart = GetTime(); } else { m_replay = 1; m_pauseTime += GetTime() - m_pauseStart; m_traceView.finished = false; SetTimer(m_hwnd, 0, 200, NULL); } } void Visualizer::StartDragToScroll(const POINT& anchor) { // Uses reference-counter method since multiple input events (left and middle mouse button) can trigger the drag-to-scroll mechansim if (m_dragToScrollCounter == 0) { m_processSelected = false; m_sessionSelectedIndex = ~0u; m_statsSelected = false; m_activeProcessGraphSelected = false; m_buttonSelected = ~0u; m_timelineSelected = 0; m_fetchedFilesSelected = ~0u; m_workSelected = false; m_hyperLinkSelected.clear(); m_autoScroll = false; m_mouseAnchor = { anchor.x, anchor.y }; m_scrollAtAnchorX = m_scrollPosX; m_scrollAtAnchorY = m_scrollPosY; SetCapture(m_hwnd); Redraw(false); } ++m_dragToScrollCounter; } void Visualizer::StopDragToScroll() { if (m_dragToScrollCounter > 0) --m_dragToScrollCounter; if (m_dragToScrollCounter != 0) return; ReleaseCapture(); if (UpdateSelection()) Redraw(false); } void Visualizer::SaveSettings() { RECT rect; GetWindowRect(m_hwnd, &rect); m_config.x = rect.left; m_config.y = rect.top; m_config.width = rect.right - rect.left; m_config.height = rect.bottom - rect.top; m_config.Save(m_logger); } void Visualizer::DirtyBitmaps(bool full) { for (auto& session : m_traceView.sessions) for (auto& processor : session.processors) for (auto& process : processor.processes) { process.bitmapDirty = true; if (full) process.bitmap = 0; } for (auto& workTrack : m_traceView.workTracks) for (auto& work : workTrack.records) { work.bitmapDirty = true; if (full) work.bitmap = 0; } if (!full) return; for (HBITMAP bm : m_textBitmaps) DeleteObject(bm); DeleteObject(m_lastBitmap); m_textBitmaps.clear(); m_lastBitmapOffset = BitmapCacheHeight; m_lastBitmap = 0; } void Visualizer::PrintCacheWriteStats(Logger& logger, u32 processId) { auto findIt = m_traceView.cacheWrites.find(processId); if (findIt == m_traceView.cacheWrites.end()) return; TraceView::CacheWrite& write = findIt->second; logger.Info(L""); logger.Info(L" -------- Cache write stats ----------"); logger.Info(L" Duration %9s", TimeToText(write.end - write.start).str); logger.Info(L" Success %9s", write.success ? L"true" : L"false"); logger.Info(L" Bytes sent %9s", BytesToText(write.bytesSent).str); } void Visualizer::UpdateFont(Font& font, int height, bool createUnderline) { font.height = height; int fh = height; font.offset = 0; if (height <= 13) { ++fh; --font.offset; } if (height <= 11) ++fh; if (height <= 9) ++fh; if (height <= 8) ++fh; if (height <= 6) ++fh; if (height <= 4) --font.offset; if (font.handle) DeleteObject(font.handle); if (font.handleUnderlined) DeleteObject(font.handleUnderlined); //NONCLIENTMETRICS nonClientMetrics; //nonClientMetrics.cbSize = sizeof(nonClientMetrics); //SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(nonClientMetrics), &nonClientMetrics, 0); //m_font = (HFONT)CreateFontIndirect(&nonClientMetrics.lfMessageFont); font.handle = CreateFontW(4 - fh, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, m_config.fontName.c_str()); if (createUnderline) font.handleUnderlined = CreateFontW(4 - fh, 0, 0, 0, FW_NORMAL, 0, 1, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, m_config.fontName.c_str()); } void Visualizer::UpdateDefaultFont() { UpdateFont(m_defaultFont, m_config.fontSize, true); m_sessionStepY = m_defaultFont.height + 4; m_timelineFont = m_defaultFont; } void Visualizer::UpdateProcessFont() { m_zoomValue = 1.0f + float(m_boxHeight) / 30.0f; int fontHeight = Max(m_boxHeight - 2, 1); UpdateFont(m_processFont, fontHeight, false); m_progressRectLeft = int(13 + float(m_processFont.height) * 1.5f); DirtyBitmaps(true); } void Visualizer::ChangeFontSize(int offset) { m_config.fontSize += offset; m_config.fontSize = Max(m_config.fontSize, 10u); UpdateDefaultFont(); Redraw(true); } void Visualizer::Redraw(bool now) { u32 flags = RDW_INVALIDATE; if (now) flags |= RDW_UPDATENOW; RedrawWindow(m_hwnd, NULL, NULL, flags); u32 activeProcessCount = u32(m_trace.m_activeProcesses.size()); for (u32 i=0; i!=sizeof_array(m_activeProcessCountHistory); ++i) m_activeProcessCountHistory[i] = activeProcessCount; } void Visualizer::PaintClient(const Function& paintFunc) { HDC hdc = GetDC(m_hwnd); RECT rect; GetClientRect(m_hwnd, &rect); HDC memDC = CreateCompatibleDC(hdc); if (!EqualRect(&m_cachedBitmapRect, &rect)) { if (m_cachedBitmap) DeleteObject(m_cachedBitmap); m_cachedBitmap = CreateCompatibleBitmap(hdc, rect.right - rect.left, rect.bottom - rect.top); m_cachedBitmapRect = rect; } HGDIOBJ oldBmp = SelectObject(memDC, m_cachedBitmap); paintFunc(hdc, memDC, rect); SelectObject(memDC, oldBmp); DeleteDC(memDC); ReleaseDC(m_hwnd, hdc); } constexpr int GraphHeight = 30; struct SessionRec { TraceView::Session* session; u32 index; }; void Populate(SessionRec* recs, TraceView& traceView, bool sort) { u32 count = u32(traceView.sessions.size()); for (u32 i = 0, e = count; i != e; ++i) recs[i] = { &traceView.sessions[i], i }; if (count <= 1 || !sort) return; std::sort(recs + 1, recs + traceView.sessions.size(), [](SessionRec& a, SessionRec& b) { auto& as = *a.session; auto& bs = *b.session; if ((as.processActiveCount != 0) != (bs.processActiveCount != 0)) return as.processActiveCount > bs.processActiveCount; if (as.processActiveCount && as.proxyCreated != bs.proxyCreated) return int(as.proxyCreated) > int(bs.proxyCreated); return a.index < b.index; }); } void Visualizer::PaintAll(HDC hdc, const RECT& clientRect) { u64 playTime = GetPlayTime(); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, m_textColor); SetBkColor(hdc, m_config.DarkMode ? RGB(70, 70, 70) : RGB(180, 180, 180)); HBRUSH lastSelectedBrush = 0; HBRUSH textLastSelectedBrush = 0; auto fillRect = [&](const RECT& r, HBRUSH b) { if (lastSelectedBrush != b) { SelectObject(hdc, b); lastSelectedBrush = b; } PatBlt(hdc, r.left, r.top, r.right - r.left, r.bottom - r.top, PATCOPY); }; auto DrawCenteredText = [&](const StringBuffer<>* lines, u32 lineCount, bool drawBackground) { for (u32 i=0; i!=lineCount; ++i) { RECT rect = { 0, 0, 0, 0 }; DrawTextW(hdc, lines[i].data, lines[i].count, &rect, DT_SINGLELINE|DT_NOPREFIX|DT_NOCLIP|DT_CALCRECT); int top = (clientRect.bottom - rect.bottom)/2 + i*20; rect.left = (clientRect.right - rect.right)/2; rect.right = (clientRect.right + rect.right)/2; rect.bottom = top + (rect.bottom - rect.top); rect.top = top; if (drawBackground) { RECT r = rect; InflateRect(&r, 4, 2); fillRect(r, m_tooltipBackgroundBrush); } DrawTextW(hdc, lines[i].data, lines[i].count, &rect, DT_SINGLELINE|DT_NOPREFIX|DT_NOCLIP); } }; if (m_traceView.sessions.empty() && !m_parentHwnd) { StringBuffer<> str[2]; if (!m_fileName.IsEmpty()) { str[0].Append(TCV("Loading trace file")); } else { str[1].Append(TCV("Click here to open trace file")); if (m_listenChannel.count) { str[0].Appendf(TC("Listening for new sessions on channel '%s'"), m_listenChannel.data); str[1].Append(TCV(" instead")); } else str[0].Append(TCV("No trace active")); } SetActiveFont(m_popupFont); DrawCenteredText(str, 2, false); return; } int posY = int(m_scrollPosY); float scaleX = 50.0f*m_zoomValue*m_horizontalScaleValue; RECT progressRect = clientRect; progressRect.left += m_progressRectLeft; if (m_config.showTimeline) { progressRect.bottom -= m_defaultFont.height + 10; } HDC textDC = CreateCompatibleDC(hdc); SetTextColor(textDC, m_textColor); SelectObject(textDC, m_processFont.handle); SelectObject(textDC, GetStockObject(NULL_BRUSH)); SetBkMode(textDC, TRANSPARENT); //TEXTMETRIC metric; //GetTextMetrics(textDC, &metric); HBITMAP nullBmp = CreateCompatibleBitmap(hdc, 1, 1); HBITMAP oldBmp = (HBITMAP)SelectObject(textDC, nullBmp); HBITMAP lastSelectedBitmap = 0; u64 lastStop = 0; SetActiveFont(m_defaultFont); auto drawStatusText = [&](const StringView& text, LogEntryType type, int posX, int endX, bool moveY, bool underlined = false) { RECT rect; rect.left = posX; rect.right = endX; rect.top = posY + m_activeFont.offset; rect.bottom = posY + m_activeFont.height + 2; SetTextColor(hdc, type == LogEntryType_Info ? m_textColor : (type == LogEntryType_Error ? m_textErrorColor : m_textWarningColor)); if (underlined) SelectObject(m_activeHdc, m_activeFont.handleUnderlined); ExtTextOutW(hdc, rect.left, posY, ETO_CLIPPED, &rect, text.data, text.count, NULL); if (underlined) SelectObject(m_activeHdc, m_activeFont.handle); if (moveY) posY = rect.bottom; }; auto drawIndentedText = [&](const StringView& text, LogEntryType type, int indent, bool moveY, bool underlined = false) { int posX = 5 + indent*m_defaultFont.height; drawStatusText(text, type, posX, clientRect.right, moveY, underlined); }; if (m_config.showProgress && m_traceView.progressProcessesTotal) { drawIndentedText(AsView(L"Progress"), LogEntryType_Info, 1, false); float progress = float(m_traceView.progressProcessesDone) / float(m_traceView.progressProcessesTotal); u32 width = m_activeFont.height * 18; RECT rect; rect.left = 3 + 6*m_activeFont.height; rect.right = rect.left + width; rect.top = posY; rect.bottom = posY + m_activeFont.height; fillRect(rect, m_processBrushes[0].inProgress); rect.right = rect.left + int(progress*float(width));//durationMs/100; fillRect(rect, m_traceView.progressErrorCount ? m_processBrushes[0].error : m_processBrushes[0].success); StringBuffer<> str; str.Appendf(TC("%u%% %u / %u"), u32(progress*100.0f), m_traceView.progressProcessesDone, m_traceView.progressProcessesTotal); const tchar* remoteDisabled = TC(""); // TODO: Only show this in "advanced mode" m_traceView.remoteExecutionDisabled ? TC(" (Remote spawn disabled)") : TC(""); str.Append(remoteDisabled); if (u32 active = m_traceView.totalProcessActiveCount) str.Appendf(TC(" (%u active)"), active); drawIndentedText(str, LogEntryType_Info, 6, true); } if (m_traceView.version && (m_traceView.version < TraceReadCompatibilityVersion || m_traceView.version > TraceVersion)) { if (!m_traceView.finished) { SetTextColor(hdc, m_textWarningColor); StringBuffer<> str; str.Appendf(TC("Unsupported trace version %u (Versions supported are %u to %u)"), m_traceView.version, TraceReadCompatibilityVersion, TraceVersion); HFONT handle = CreateFontW(-20, 0, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, m_config.fontName.c_str()); SelectObject(hdc, handle); DrawTextW(hdc, str.data, str.count, (RECT*)&clientRect, DT_CENTER|DT_NOPREFIX|DT_SINGLELINE|DT_VCENTER); DeleteObject(handle); } else { m_traceView.Clear(); StringBuffer<> title; SetWindowTextW(m_hwnd, GetTitlePrefix(title).Appendf(L" (Listening for new sessions on channel '%s')", m_listenChannel.data).data); } return; } if (m_config.showStatus && !m_traceView.statusMap.empty()) { u32 lastRow = ~0u; u32 row = ~0u; for (auto& kv : m_traceView.statusMap) { auto& status = kv.second; if (status.text.empty()) continue; row = u32(kv.first >> 32); if (lastRow != ~0u && lastRow != row) posY += m_activeFont.height + 2; lastRow = row; u32 column = u32(kv.first & ~0u); drawIndentedText(status.text, status.type, column, false, !status.link.empty()); } if (row != ~0u) posY += m_activeFont.height + 2; SetTextColor(hdc, m_textColor); posY += 3; } if (m_config.showActiveProcessGraph) { if (posY + GraphHeight >= progressRect.top && posY + GraphHeight - 5 < progressRect.bottom) { int graphBaseY = posY + GraphHeight - 4; double graphHeight = double(GraphHeight - 2); Vector line; SelectObject(hdc, m_activeProcPen); bool isFirstUpdate = true; bool isFirstDraw = true; u64 prevValue = 0; int prevX = 0; int prevY = 0; float timeScale = (m_horizontalScaleValue*m_zoomValue)*50.0f; float startOffset = ((m_scrollPosX/timeScale) - float(int(m_scrollPosX/timeScale))) * timeScale; float time = -startOffset/timeScale; float playTimeS = TimeToS(playTime); u32 activeProcessCountIndex = 0; u16 count = 0; while (time < playTimeS) { int posX = progressRect.left + int(startOffset + time*timeScale); if (posX >= clientRect.right) break; while (activeProcessCountIndex < m_traceView.activeProcessCounts.size()) { auto& entry = m_traceView.activeProcessCounts[activeProcessCountIndex]; count = entry.count; if (float(TimeToMs(entry.time))/1000.0f > time) break; ++activeProcessCountIndex; } int x = posX; int y = graphBaseY; double div = double(m_traceView.maxActiveProcessCount); y -= int(double(count) * graphHeight / div); if (prevValue <= 0) prevY = y; if (x > clientRect.left) { if (isFirstUpdate) { line.push_back({ x, y }); isFirstDraw = false; } else { if (isFirstDraw) line.push_back({ prevX, prevY }); line.push_back({ x, y }); isFirstDraw = false; } } if (x > clientRect.right) break; isFirstUpdate = false; prevX = x; prevY = y; prevValue = count; time += 0.25f; } if (line.size() > 1) Polyline(hdc, line.data(), int(line.size())); } posY += GraphHeight; } if (m_config.showActiveProcesses && !m_trace.m_activeProcesses.empty()) { auto drawBox = [&](u64 start, u64 stop, int height, bool selected, bool inProgress) { //int left = 3 + 6*m_activeFont.height; //int right = rect.left + durationMs/100; int posX = int(m_scrollPosX) + progressRect.left; bool done = stop != ~u64(0); if (!done) stop = playTime; int left = posX + int(float(TimeToS(start)) * scaleX); int right = posX + int(float(TimeToS(stop)) * scaleX) - 1; RECT rect; rect.left = left; rect.right = right; rect.top = posY; rect.bottom = posY + height; fillRect(rect, inProgress ? m_processBrushes[selected].inProgress : m_processBrushes[selected].success); return rect; }; m_activeProcessCountHistory[m_activeProcessCountHistoryIterator++ % sizeof_array(m_activeProcessCountHistory)] = u32(m_trace.m_activeProcesses.size()); PaintActiveProcesses(posY, clientRect, [&](TraceView::ProcessLocation& processLocation, u32 boxHeight, bool firstWithHeight) { auto& session = m_trace.GetSession(m_traceView, processLocation.sessionIndex); TraceView::Process& process = session.processors[processLocation.processorIndex].processes[processLocation.processIndex]; bool selected = m_processSelected && m_processSelectedLocation == processLocation; if (m_config.showFinishedProcesses) { u32 index = processLocation.processIndex; while (index > 0) { --index; TraceView::Process& process2 = session.processors[processLocation.processorIndex].processes[index]; drawBox(process2.start, process2.stop, boxHeight, false, false); } } RECT boxRect = drawBox(process.start, process.stop, boxHeight, selected, true); u32 v = boxHeight - 1; if (v > 4) { u32 fontIndex = Min(v, u32(sizeof_array(m_activeProcessFont) - 1)); if (!m_activeProcessFont[fontIndex].handle) { UpdateFont(m_activeProcessFont[fontIndex], fontIndex - 1, false); m_activeProcessFont[fontIndex].offset += 1; } if (firstWithHeight) SetActiveFont(m_activeProcessFont[fontIndex]); StringBuffer<> str; auto firstPar = -1;//process.description.find_first_of('('); if (firstPar != -1 && *process.description.rbegin() == ')') { str.Append(process.description.c_str() + firstPar + 1).Resize(str.count-1); str.Append(L" ").Append(process.description.c_str(), firstPar); } else str.Append(process.description); if (process.isRemote) str.Append(L" [").Append(session.name).Append(']'); else if (process.cacheFetch) str.Append(L" [cache]"); if (boxRect.left < 0) str.Appendf(L" %s", TimeToText(playTime - process.start, true).str); //drawIndentedText(str, LogEntryType_Info, 1, true); drawStatusText(str, LogEntryType_Info, Max(int(boxRect.left+1), 1), boxRect.right, false); } }); } int boxHeight = m_boxHeight; int stepY = int(boxHeight) + 2; int processStepY = boxHeight + 1; TraceView::WorkRecord selectedWork; SessionRec sortedSessions[1024]; Populate(sortedSessions, m_traceView, m_config.SortActiveRemoteSessions); u32 visibleBoxes = 0; TraceView::ProcessLocation processLocation { 0, 0, 0 }; for (u64 sessionIt = 0, sessionEnd = m_traceView.sessions.size(); sessionIt != sessionEnd; ++sessionIt) { bool isFirst = sessionIt == 0; auto& session = *sortedSessions[sessionIt].session; bool hasUpdates = !session.updates.empty(); if (!isFirst) { if (!hasUpdates && session.processors.empty()) continue; if (!m_config.showFinishedProcesses && session.disconnectTime != ~u64(0)) continue; } processLocation.sessionIndex = sortedSessions[sessionIt].index; if (!isFirst) posY += 3; if (m_config.showTitleBars) { if (posY + stepY >= progressRect.top && posY <= progressRect.bottom) { SelectObject(hdc, m_separatorPen); MoveToEx(hdc, 0, posY, NULL); LineTo(hdc, clientRect.right, posY); StringBuffer<> text; text.Append(session.fullName); if (hasUpdates && session.disconnectTime == ~u64(0)) { u64 ping = session.ping.back(); //u64 memAvail = session.updates.back().memAvail; //float cpuLoad = session.updates.back().cpuLoad; //text.Appendf(L" - Cpu: %.1f%%", cpuLoad * 100.0f); //if (memAvail) // text.Appendf(L" Mem: %ls/%ls", BytesToText(session.memTotal - memAvail).str, BytesToText(session.memTotal).str); if (ping) text.Appendf(L" Ping: %ls", TimeToText(ping, false, m_traceView.frequency).str); if (!session.notification.empty()) text.Append(L" - ").Append(session.notification); } else if (!isFirst) { text.Append(L" - Disconnected"); if (!session.notification.empty()) text.Append(L" (").Append(session.notification).Append(')'); } bool selected = m_sessionSelectedIndex == processLocation.sessionIndex; int textBottom = Min(posY + m_sessionStepY, int(progressRect.bottom)); RECT rect; rect.left = 5; rect.right = clientRect.right; rect.top = posY; rect.bottom = textBottom; if (selected) SetBkMode(hdc, OPAQUE); SIZE textSize; if (GetTextExtentPoint32W(hdc, text.data, text.count, &textSize)) session.fullNameWidth = u32(textSize.cx); ExtTextOutW(hdc, 5, posY+2, ETO_CLIPPED, &rect, text.data, text.count, NULL); if (selected) SetBkMode(hdc, TRANSPARENT); } posY += m_sessionStepY; } bool showGraph = m_config.showNetworkStats || m_config.showCpuMemStats || m_config.showDriveStats; if (showGraph && hasUpdates) { if (posY + GraphHeight >= progressRect.top && posY + GraphHeight - 5 < progressRect.bottom) { int posX = int(m_scrollPosX) + progressRect.left; int graphBaseY = posY + GraphHeight - 4; double graphHeight = double(GraphHeight - 2); Vector line; auto drawGraph = [&](auto& values, auto maxValue, double scale, HPEN pen, bool accumulatingValue, int offsetY = 0) { bool loop = true; u32 reconnectIndex = 0; while (loop) { SelectObject(hdc, pen); bool isFirstUpdate = true; bool isFirstDraw = true; decltype(maxValue) prevValue = 0; u64 prevTime = 0; int prevX = 0; int prevY = 0; u64 i = 0; u64 e = 0; if (reconnectIndex) i = session.reconnectIndices[reconnectIndex - 1]; if (reconnectIndex < session.reconnectIndices.size()) e = session.reconnectIndices[reconnectIndex]; else { e = Min(session.updates.size(), values.size()); loop = false; } line.clear(); for (; i!=e; ++i) { u64 updateTime = session.updates[i]; auto value = values[i]; int x = posX + int(float(TimeToS(updateTime)) * scaleX); int y = graphBaseY; double duration = TimeToS(updateTime - prevTime); if (updateTime == 0) isFirstUpdate = true; if (accumulatingValue) { double invScaleY = duration * scale; if (invScaleY != 0 && prevValue) y -= int(double(value - prevValue) / invScaleY) + offsetY; } else { double div = 1.0f; if (maxValue) div = double(maxValue); y -= int(double(maxValue - value*scale)*graphHeight/div) + offsetY; } if (prevValue <= 0) prevY = y; if (x > clientRect.left) { if (isFirstUpdate) { line.push_back({x, y}); //MoveToEx(hdc, x, y, NULL); isFirstDraw = false; } else { if (isFirstDraw) line.push_back({prevX, prevY}); // MoveToEx(hdc, prevX, prevY, NULL); line.push_back({x, y}); // LineTo(hdc, x, y); isFirstDraw = false; } } if (x > clientRect.right) break; isFirstUpdate = false; prevX = x; prevY = y; prevValue = value; prevTime = updateTime; } if (line.size() > 1) Polyline(hdc, line.data(), int(line.size())); ++reconnectIndex; } }; if (m_config.showNetworkStats && session.highestSendPerS != 0 && session.highestRecvPerS != 0) { drawGraph(session.networkSend, 100000000000000ull, double(session.highestSendPerS) / graphHeight, m_sendPen, true); drawGraph(session.networkRecv, 100000000000000ull, double(session.highestRecvPerS) / graphHeight, m_recvPen, true, 1); } if (m_config.showCpuMemStats) { drawGraph(session.cpuLoad, 0.0f, -1.0, m_cpuPen, false); drawGraph(session.memAvail, session.memTotal, 1.0, m_memPen, false); } if (m_config.showDriveStats) for (auto& pair : session.drives) if (pair.second.busyHighest) drawGraph(pair.second.busyPercent, 0, -0.01, m_drivePen, false); } posY += GraphHeight; } if (m_config.showDetailedData) { auto drawText = [&](const StringBufferBase& text, RECT& rect, u32* outWidth) { if (rect.top > progressRect.bottom) return; bool selected = m_fetchedFilesSelected == processLocation.sessionIndex && text.StartsWith(TC("Fetched Files")); if (selected) SetBkMode(hdc, OPAQUE); if (rect.bottom > progressRect.bottom) { rect.bottom = progressRect.bottom; ExtTextOutW(hdc, rect.left, rect.top, ETO_CLIPPED, &rect, text.data, text.count, NULL); } else ExtTextOutW(hdc, rect.left, rect.top, 0, NULL, text.data, text.count, NULL); if (selected) SetBkMode(hdc, TRANSPARENT); if (outWidth) { SIZE s; GetTextExtentPoint32W(hdc, text.data, text.count, &s); *outWidth = u32(s.cx); } }; bool isRemote = processLocation.sessionIndex != 0; PaintDetailedStats(posY, progressRect, session, isRemote, playTime, drawText); } SetActiveFont(m_processFont); bool shouldDrawText = m_processFont.height > 4; if (m_config.showProcessBars) { processLocation.processorIndex = 0; for (auto& processor : session.processors) { bool drawProcessorIndex = m_config.showFinishedProcesses; if (posY + m_sessionStepY >= progressRect.top && posY < progressRect.bottom) { int barHeight = boxHeight; int textOffsetY = 0; if (posY + boxHeight > progressRect.bottom) { int newBarHeight = Min(barHeight, int(progressRect.bottom - posY)); textOffsetY = int(barHeight - newBarHeight); barHeight = newBarHeight; } const int textHeight = barHeight; const int rectBottom = posY + textHeight; const int offsetY = (textHeight - m_processFont.height + textOffsetY) / 2; processLocation.processIndex = 0; int posX = int(m_scrollPosX) + progressRect.left; for (auto& process : processor.processes) { int left = posX + int(float(TimeToS(process.start)) * scaleX); auto pig = MakeGuard([&]() { ++processLocation.processIndex; }); if (left >= progressRect.right) continue; u64 stop = process.stop; bool done = stop != ~u64(0); if (!done) stop = playTime; else if (!m_config.showFinishedProcesses) continue; drawProcessorIndex = true; RECT rect; rect.left = left; rect.right = posX + int(float(TimeToS(stop)) * scaleX) - 1; rect.top = posY; rect.bottom = rectBottom - 1; if (rect.right <= progressRect.left) continue; if (!m_filterString.empty() && !Contains(process.description.c_str(), m_filterString.c_str()) && !Contains(process.breadcrumbs.c_str(), m_filterString.c_str())) continue; ++visibleBoxes; rect.right = Max(int(rect.right), left + 1); bool selected = m_processSelected && m_processSelectedLocation == processLocation; if (selected) process.bitmapDirty = true; --rect.top; PaintProcessRect(process, hdc, rect, progressRect, selected, false, lastSelectedBrush); ++rect.top; int processWidth = rect.right - rect.left; if (shouldDrawText && m_config.ShowProcessText && processWidth > 3) { if (!process.bitmap || process.bitmapDirty) { if (!process.bitmap) { if (m_lastBitmapOffset == BitmapCacheHeight) { if (m_lastBitmap) m_textBitmaps.push_back(m_lastBitmap); m_lastBitmapOffset = 0; m_lastBitmap = CreateCompatibleBitmap(hdc, 256, BitmapCacheHeight); } process.bitmap = m_lastBitmap; process.bitmapOffset = m_lastBitmapOffset; m_lastBitmapOffset += m_processFont.height; } if (lastSelectedBitmap != process.bitmap) { SelectObject(textDC, process.bitmap); lastSelectedBitmap = process.bitmap; } RECT rect2{ 0, int(process.bitmapOffset), 256, int(process.bitmapOffset) + m_processFont.height }; RECT rect3{ 0, int(process.bitmapOffset), processWidth, int(process.bitmapOffset) + m_processFont.height }; if (!done) rect3.right = 256; PaintProcessRect(process, textDC, rect3, rect2, selected, true, textLastSelectedBrush); rect2.left += 3; // Move in text a bit int textY = rect2.top + m_processFont.offset; bool dropShadow = m_config.DarkMode; if (dropShadow) { SetTextColor(textDC, RGB(5, 60, 5)); ++rect2.left; ++textY; ExtTextOutW(textDC, rect2.left, textY, ETO_CLIPPED, &rect2, process.description.c_str(), int(process.description.size()), NULL); --rect2.left; --textY; } SetTextColor(textDC, m_textColor); ExtTextOutW(textDC, rect2.left, textY, ETO_CLIPPED, &rect2, process.description.c_str(), int(process.description.size()), NULL); if (!selected) process.bitmapDirty = false; } if (lastSelectedBitmap != process.bitmap) { SelectObject(textDC, process.bitmap); lastSelectedBitmap = process.bitmap; } int width = Min(processWidth, 256); int bitmapOffsetY = process.bitmapOffset; int bltOffsetY = offsetY; if (bltOffsetY < 0) { bitmapOffsetY -= bltOffsetY; bltOffsetY = 0; } int height = Min(textHeight, m_processFont.height); if (bltOffsetY + height > textHeight) height = textHeight - bltOffsetY; if (left > -256 && height >= 0) { int bitmapOffsetX = rect.left - left; if (left < progressRect.left) { int diff = progressRect.left - left; rect.left = progressRect.left; width -= diff; bitmapOffsetX += diff; } BitBlt(hdc, rect.left, rect.top + bltOffsetY, width, height, textDC, bitmapOffsetX, bitmapOffsetY, SRCCOPY); } } } if (drawProcessorIndex) { RECT rect; rect.left = 5; rect.right = progressRect.left - 2; rect.top = posY; rect.bottom = rectBottom; StringBuffer<> buf; buf.AppendValue(u64(processLocation.processorIndex) + 1); ExtTextOutW(hdc, 5, posY + offsetY, ETO_CLIPPED, &rect, buf.data, buf.count, NULL); } } lastStop = Max(lastStop, processor.processes.rbegin()->stop); ++processLocation.processorIndex; if (drawProcessorIndex) posY += processStepY; } } else { for (auto& processor : session.processors) if (!processor.processes.empty()) lastStop = Max(lastStop, processor.processes.rbegin()->stop); } if (m_config.showWorkers && isFirst) { u32 trackIndex = 0; for (auto& workTrack : m_traceView.workTracks) { if (posY + m_sessionStepY >= progressRect.top && posY <= progressRect.bottom) { int textOffsetY = 0; int barHeight = boxHeight; if (posY + int(boxHeight) > progressRect.bottom) { int newBarHeight = Min(barHeight, int(progressRect.bottom - posY)); textOffsetY = barHeight - newBarHeight; barHeight = newBarHeight; } const int textHeight = barHeight; const int rectBottom = posY + textHeight; const int offsetY = (textHeight - m_processFont.height + textOffsetY) / 2; if (shouldDrawText) { RECT rect; rect.left = 5; rect.right = progressRect.left - 5; rect.top = posY; rect.bottom = rectBottom; StringBuffer<> buf; buf.AppendValue(u64(trackIndex) + 1); ExtTextOutW(hdc, 5, posY + offsetY, ETO_CLIPPED, &rect, buf.data, buf.count, NULL); } int lastDrawnRight = 0; u32 workIndex = 0; int posX = int(m_scrollPosX) + progressRect.left; for (auto& work : workTrack.records) { auto inc = MakeGuard([&](){ ++workIndex; }); if (work.start == work.stop) continue; float startTime = TimeToS(work.start); int left = posX + int(startTime * scaleX); if (left >= progressRect.right) continue; if (!m_filterString.empty()) { bool keep = Contains(work.description, m_filterString.c_str()); if (!keep) for (auto& en : work.entries) keep |= Contains(en.text, m_filterString.c_str()); if (!keep) continue; } HBRUSH brush; u64 stop = work.stop; float stopTime = TimeToS(stop); RECT rect; rect.left = left; rect.right = posX + int(stopTime * scaleX) - 1; rect.top = posY; rect.bottom = rectBottom - 1; if (rect.right <= progressRect.left) continue; ++visibleBoxes; rect.right = Max(int(rect.right), left + 1); Color color = work.color; bool selected = m_workSelected && m_workTrack == trackIndex && m_workIndex == workIndex; if (selected) { selectedWork = work; work.bitmapDirty = true; auto c = (u8*)&color; for (u32 j=0; j!=3; ++j) c[j] = u8(Min(int(c[j]) + 40, 255)); } else { if (rect.left + 1 == rect.right) if (lastDrawnRight == rect.right) continue; } lastDrawnRight = rect.right; bool done = stop != ~u64(0); if (done) { auto insres = m_coloredBrushes.try_emplace(color); if (insres.second) { auto c = (u8*)&color; insres.first->second = CreateSolidBrush(RGB(c[2], c[1], c[0])); } brush = insres.first->second; } else { stop = playTime; brush = m_processBrushes[0].inProgress; } --rect.top; auto clampRect = [&](RECT& r) { r.left = Min(Max(r.left, progressRect.left), progressRect.right); r.right = Max(Min(r.right, progressRect.right), progressRect.left); }; clampRect(rect); fillRect(rect, brush); ++rect.top; int processWidth = rect.right - rect.left; if (shouldDrawText && m_config.ShowProcessText && processWidth > 3) { if (!work.bitmap || work.bitmapDirty) { if (!work.bitmap) { if (m_lastBitmapOffset == BitmapCacheHeight) { if (m_lastBitmap) m_textBitmaps.push_back(m_lastBitmap); m_lastBitmapOffset = 0; m_lastBitmap = CreateCompatibleBitmap(hdc, 256, BitmapCacheHeight); } work.bitmap = m_lastBitmap; work.bitmapOffset = m_lastBitmapOffset; m_lastBitmapOffset += m_processFont.height; } if (lastSelectedBitmap != work.bitmap) { SelectObject(textDC, work.bitmap); lastSelectedBitmap = work.bitmap; } RECT rect2{ 0,int(work.bitmapOffset), 256, int(work.bitmapOffset) + m_processFont.height }; SelectObject(textDC, brush); PatBlt(textDC, rect2.left, rect2.top, rect2.right - rect2.left, rect2.bottom - rect2.top, PATCOPY); ExtTextOutW(textDC, rect2.left, rect2.top, ETO_CLIPPED, &rect2, work.description, int(wcslen(work.description)), NULL); if (!selected) work.bitmapDirty = false; } else if (lastSelectedBitmap != work.bitmap) { SelectObject(textDC, work.bitmap); lastSelectedBitmap = work.bitmap; } int width = Min(processWidth, 256); int bitmapOffsetY = work.bitmapOffset; int bltOffsetY = offsetY; if (bltOffsetY < 0) { bitmapOffsetY -= bltOffsetY; bltOffsetY = 0; } int height = Min(textHeight, m_processFont.height); if (bltOffsetY + height > textHeight) height = textHeight - bltOffsetY; if (left > -256 && height >= 0) { int bitmapOffsetX = rect.left - left; if (left < progressRect.left) { int diff = progressRect.left - left; rect.left = progressRect.left; width -= diff; bitmapOffsetX += diff; } BitBlt(hdc, rect.left, rect.top + bltOffsetY, width, height, textDC, bitmapOffsetX, bitmapOffsetY, SRCCOPY); } } } } ++trackIndex; posY += processStepY; } } SetActiveFont(m_defaultFont); } SelectObject(textDC, oldBmp); DeleteObject(nullBmp); DeleteDC(textDC); m_contentWidth = m_progressRectLeft + Max(0, int(TimeToS((lastStop != 0 && lastStop != ~u64(0)) ? lastStop : playTime) * scaleX)); m_contentHeight = posY - int(m_scrollPosY) + stepY + 14; float timelineSelected = m_timelineSelected; if (m_config.showTimeline && !m_traceView.sessions.empty()) PaintTimeline(hdc, clientRect); if (!m_filterString.empty()) { StringBuffer<> str[2]; str[0].Append(TCV("Box Filter: ")).Append(m_filterString); str[1].Appendf(TC("Box Count: %u"), visibleBoxes); SetActiveFont(m_popupFont); DrawCenteredText(str, 2, true); } if (m_config.showCursorLine && m_mouseOverWindow) { float timeScale = (m_horizontalScaleValue * m_zoomValue)*50.0f; float startOffset = -(m_scrollPosX / timeScale); POINT pos; GetCursorPos(&pos); ScreenToClient(m_hwnd, &pos); timelineSelected = startOffset + float(pos.x - m_progressRectLeft) / timeScale; } if (timelineSelected != 0) { int posX = int(m_scrollPosX) + progressRect.left; int left = posX + int(timelineSelected * scaleX); int timelineTop = GetTimelineTop(clientRect); // TODO: Draw line up MoveToEx(hdc, left, 2, NULL); LineTo(hdc, left, timelineTop); if (timelineSelected >= 0) { StringBuffer<> b; u32 milliseconds = u32(timelineSelected * 1000.0f); u32 seconds = milliseconds / 1000; milliseconds -= seconds * 1000; u32 minutes = seconds / 60; seconds -= minutes * 60; u32 hours = minutes / 60; minutes -= hours * 60; if (hours) { b.AppendValue(hours).Append('h'); if (minutes < 10) b.Append('0'); } if (minutes || hours) { b.AppendValue(minutes).Append('m'); if (seconds < 10) b.Append('0'); } b.AppendValue(seconds).Append('.'); if (milliseconds < 100) b.Append('0'); if (milliseconds < 10) b.Append('0'); b.AppendValue(milliseconds); StringBuffer<128> wt = GetWorldTime(timelineSelected); SetActiveFont(m_popupFont); DrawTextLogger logger(m_hwnd, hdc, m_popupFont.height, m_tooltipBackgroundBrush); logger.Info(L"%s (%s)", b.data, wt.data); logger.DrawAtPos(left + 4, timelineTop - 20); } } { int boxSide = 8; int boxStride = boxSide + 2; int top = 5; int bottom = top + boxSide; int left = progressRect.right - 7 - boxSide; int right = progressRect.right - 7; bool* values = &m_config.showProgress; for (int i = VisualizerFlag_Count - 1; i >= 0; --i) { SelectObject(hdc, m_buttonSelected == u32(i) ? m_textPen : m_checkboxPen); SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, left, top, right, bottom); if (values[i]) { MoveToEx(hdc, left + 2, top + 2, NULL); LineTo(hdc, right - 2, bottom - 2); MoveToEx(hdc, right - 3, top + 2, NULL); LineTo(hdc, left + 1, bottom - 2); } left -= boxStride; right -= boxStride; } top -= 2; auto drawText = [&](const tchar* text, COLORREF color) { SetTextColor(hdc, color); RECT r{left, top, left+200, top+200}; auto strLen = TStrlen(text); DrawTextW(hdc, text, strLen, &r, DT_SINGLELINE|DT_NOCLIP|DT_NOPREFIX|DT_CALCRECT); left -= (r.right - r.left) + 5; r.left = left; DrawTextW(hdc, text, strLen, &r, DT_SINGLELINE|DT_NOCLIP|DT_NOPREFIX); }; if (m_config.showDriveStats) { SetActiveFont(m_defaultFont); drawText(L"DRV", m_driveColor); } if (m_config.showNetworkStats) { SetActiveFont(m_defaultFont); drawText(L"SND", m_sendColor); drawText(L"RCV", m_recvColor); } if (m_config.showCpuMemStats) { SetActiveFont(m_defaultFont); drawText(L"CPU", m_cpuColor); drawText(L"MEM", m_memColor); } SetTextColor(hdc, m_textColor); } if (m_processSelected) { const TraceView::Process& process = m_traceView.GetProcess(m_processSelectedLocation); u64 duration = 0; Vector logLines; u32 maxCharCount = 50u; bool hasExited = process.stop != ~u64(0); if (hasExited) { duration = process.stop - process.start; if (!process.logLines.empty()) { u32 lineMaxCount = 0; for (auto& line : process.logLines) { u32 offset = 0; u32 left = u32(line.text.size()); while (left) { u32 toCopy = Min(left, maxCharCount); lineMaxCount = Max(lineMaxCount, toCopy); logLines.push_back(line.text.substr(offset, toCopy)); offset += toCopy; left -= toCopy; } } } } else { duration = playTime - process.start; } SetActiveFont(m_popupFont); DrawTextLogger logger(m_hwnd, hdc, m_popupFont.height, m_tooltipBackgroundBrush); logger.AddTextOffset(-10); // Remove spaces in the front logger.AddWidth(3); logger.AddSpace(2); logger.Info(L" %ls", process.description.c_str()); logger.Info(L" Host: %ls", m_processSelectedLocation.sessionIndex == 0 ? TC("local") : m_traceView.GetSession(m_processSelectedLocation).name.c_str()); logger.Info(L" ProcessId: %6u", process.id); logger.Info(L" Start: %7ls (%ls)", TimeToText(process.start, true).str, GetWorldTime(process.start).data); logger.Info(L" Duration: %7ls", TimeToText(duration, true).str); if (!process.returnedReason.empty()) logger.Info(L" Returned: %7s", process.returnedReason.data()); if (hasExited && process.exitCode != 0) { if (process.exitCode == ProcessCancelExitCode) logger.Info(L" ExitCode: Cancelled"); else logger.Info(L" ExitCode: %7u", process.exitCode); } const auto& breadcrumbs = process.breadcrumbs; if (!breadcrumbs.empty()) { constexpr TString::size_type maxLineLen = 50; logger.Info(L""); logger.Info(L" ------------ Breadcrumbs ------------"); for (TString::size_type lineStart = 0, lineEnd = 0; lineEnd < breadcrumbs.size(); lineStart = lineEnd + 1) { // Log each individual line lineEnd = breadcrumbs.find(L'\n', lineStart); TString line = (lineEnd == TString::npos ? breadcrumbs.substr(lineStart) : breadcrumbs.substr(lineStart, lineEnd - lineStart)); // Break each line down into smaller section if they are longer than the maximum allowed length if (line.size() > maxLineLen) { for (TString::size_type sectionStart = 0, sectionEnd = 0; sectionStart < line.size(); sectionStart = sectionEnd) { const TString::size_type maxSectionLen = sectionStart == 0 ? maxLineLen : maxLineLen - 2; sectionEnd = std::min(sectionEnd + maxSectionLen, line.size()); TString section = (sectionStart == 0 ? L" " : L" ") + line.substr(sectionStart, sectionEnd - sectionStart); logger.Info(section.c_str()); } } else { line = L" " + line; logger.Info(line.c_str()); } } } if (process.stop != ~u64(0) && !process.stats.empty()) { BinaryReader reader(process.stats.data(), 0, process.stats.size()); ProcessStats processStats; SessionStats sessionStats; StorageStats storageStats; KernelStats kernelStats; CacheStats cacheStats; if (process.cacheFetch) { if (!process.returnedReason.empty()) logger.Info(L" Cache: Miss"); else logger.Info(L" Cache: Hit"); cacheStats.Read(reader, m_traceView.version); if (reader.GetLeft()) { storageStats.Read(reader, m_traceView.version); kernelStats.Read(reader, m_traceView.version); } } else { processStats.Read(reader, m_traceView.version); if (reader.GetLeft()) { if (process.isRemote || (m_traceView.version >= 36 && !process.isReuse)) sessionStats.Read(reader, m_traceView.version); storageStats.Read(reader, m_traceView.version); kernelStats.Read(reader, m_traceView.version); } } if (processStats.hostTotalTime) { logger.Info(L""); logger.Info(L" ----------- Detours stats -----------"); processStats.Print(logger, m_traceView.frequency); } else if (processStats.peakMemory) { logger.Info(L""); logger.Info(L" ----------- Process stats -----------"); processStats.Print(logger, m_traceView.frequency); } if (!sessionStats.IsEmpty()) { logger.Info(L""); logger.Info(L" ----------- Session stats -----------"); sessionStats.Print(logger, m_traceView.frequency); } if (!cacheStats.IsEmpty()) { logger.Info(L""); logger.Info(L" ------------ Cache stats ------------"); cacheStats.Print(logger, m_traceView.frequency); } if (!storageStats.IsEmpty()) { logger.Info(L""); logger.Info(L" ----------- Storage stats -----------"); storageStats.Print(logger, m_traceView.frequency); } if (!kernelStats.IsEmpty()) { logger.Info(L""); logger.Info(L" ----------- Kernel stats ------------"); kernelStats.Print(logger, false, m_traceView.frequency); } PrintCacheWriteStats(logger, process.id); if (!logLines.empty()) { logger.Info(L""); logger.Info(L" ---------------- Log ----------------"); logger.AddTextOffset(14); for (auto& line : logLines) logger.Log(LogEntryType_Info, line); } } logger.AddSpace(3); logger.DrawAtCursor(); } else if (m_workSelected && selectedWork.description) { u64 duration; if (selectedWork.stop != ~u64(0)) duration = selectedWork.stop - selectedWork.start; else duration = playTime - selectedWork.start; SetActiveFont(m_popupFont); DrawTextLogger logger(m_hwnd, hdc, m_popupFont.height, m_tooltipBackgroundBrush); logger.AddSpace(); logger.Info(L" %ls", selectedWork.description); logger.Info(L" Start: %ls (%ls)", TimeToText(selectedWork.start, true).str, GetWorldTime(selectedWork.start).data); logger.Info(L" Duration: %ls", TimeToText(duration, true).str); if (!selectedWork.entries.empty()) logger.AddSpace(); for (u32 i=0,e=u32(selectedWork.entries.size()); i!=e; ++i) { auto& entry = selectedWork.entries[i]; u64 time = 0; if (i == 0) { u64 start = entry.startTime ? entry.startTime : entry.time; if (TimeToMs(start - selectedWork.start) > 1) logger.Info(L" Start (%ls)", TimeToText(start - selectedWork.start).str); } if (entry.startTime) time = entry.time - entry.startTime; if (!time) for (u32 j=i+1;j& summary = session.summary; if (summary.empty()) { if (m_traceView.finished) logger.Info(L" Session summary was never received"); else logger.Info(L" Session summary not available until session is done"); } logger.AddTextOffset(-10); // Remove spaces in the front for (auto& line : summary) logger.Log(LogEntryType_Info, line); logger.AddTextOffset(0); for (auto& pair : session.drives) { auto& d = pair.second; logger.Info(L" %c: Read: %s (%u) Write: %s (%u)", pair.first, BytesToText(d.totalReadBytes).str, d.totalReadCount, BytesToText(d.totalWriteBytes).str, d.totalWriteCount); } logger.AddSpace(3); logger.DrawAtCursor(); } else if (m_statsSelected) { SetActiveFont(m_popupFont); DrawTextLogger logger(m_hwnd, hdc, m_popupFont.height, m_tooltipBackgroundBrush); logger.AddSpace(3); logger.SetColor(m_cpuColor).Info(L" Cpu: %.1f%%", m_stats.cpuLoad * 100.0f); logger.SetColor(m_memColor).Info(L" Mem: %ls/%ls", BytesToText(m_stats.memTotal - m_stats.memAvail).str, BytesToText(m_stats.memTotal).str); if (m_stats.recvBytes || m_stats.sendBytes) { logger.SetColor(m_recvColor).Info(L" Recv: %lsit/s", BytesToText(m_stats.recvBytesPerSecond*8).str); logger.SetColor(m_sendColor).Info(L" Send: %lsit/s", BytesToText(m_stats.sendBytesPerSecond*8).str); } if (m_stats.ping) logger.Info(L" Ping: %ls", TimeToText(m_stats.ping, false, m_traceView.frequency).str); for (auto& pair : m_stats.drives) logger.SetColor(m_driveColor).Info(L" %c: %u%% R:%s/s W:%s/s", pair.first, pair.second.busyPercent, BytesToText(pair.second.readPerSecond).str, BytesToText(pair.second.writePerSecond).str); logger.AddSpace(3); logger.DrawAtCursor(); } else if (m_activeProcessGraphSelected) { SetActiveFont(m_popupFont); DrawTextLogger logger(m_hwnd, hdc, m_popupFont.height, m_tooltipBackgroundBrush); logger.AddSpace(3); logger.AddSpace(3); logger.SetColor(m_activeProcColor).Info(L" Active Processes: %u", m_activeProcessCount); logger.DrawAtCursor(); } else if (m_buttonSelected != ~0u) { const wchar_t* tooltip[] = { #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) desc, UBA_VISUALIZER_FLAGS1 #undef UBA_VISUALIZER_FLAG }; SetActiveFont(m_popupFont); DrawTextLogger logger(m_hwnd, hdc, m_popupFont.height, m_tooltipBackgroundBrush); logger.Info(L"%ls %ls", L"Show", tooltip[m_buttonSelected]); logger.DrawAtCursor(); } else if (m_fetchedFilesSelected != ~0u) { auto& session = m_traceView.sessions[m_fetchedFilesSelected]; auto& fetchedFiles = session.fetchedFiles; if (!fetchedFiles.empty() && !fetchedFiles[0].hint.empty()) { /* int colWidth = 500; int width = colWidth * 2; int height = Min(int(clientRect.bottom), int(fetchedFiles.size() * m_popupFont.height)); SetActiveFont(m_defaultFont); DrawTextLogger logger(m_hwnd, hdc, r, m_font.height, m_tooltipBackgroundBrush); for (auto& f : fetchedFiles) { if (f.hint == TC("KnownInput")) continue; if (logger.rect.top >= r.bottom - m_font.height) { if (logger.rect.left + colWidth >= r.right) { logger.Info(L"..."); break; } logger.rect.top = r.top; logger.rect.left += colWidth; } logger.Info(L"%s", f.hint.c_str()); } logger.DrawAtCursor(); */ } } } u64 ConvertTime(const TraceView& view, u64 time); void Visualizer::PaintActiveProcesses(int& posY, const RECT& clientRect, const Function& drawProcess) { SetActiveFont(m_processFont); int startPosY = posY; Map activeProcesses; u32 remoteCount = 0; for (auto& kv : m_trace.m_activeProcesses) { auto& active = kv.second; TraceView::Process& process = m_trace.GetSession(m_traceView, active.sessionIndex).processors[active.processorIndex].processes[active.processIndex]; u64 start = process.start; //~0llu - process.start; activeProcesses.try_emplace(start, &active); if (process.isRemote) ++remoteCount; } u32 maxHeight = clientRect.bottom; bool fillHeight = ActiveProcessesShouldFillHeight(); if (fillHeight) { maxHeight = clientRect.bottom - posY; if (m_config.showTimeline) maxHeight -= GetTimelineHeight(); } else { u32 maxHeight2 = m_config.maxActiveVisible * (m_activeFont.height + 2); maxHeight = Min(maxHeight, maxHeight2); } u32 maxSize = Min(m_config.maxActiveProcessHeight, 32u); u32 maxSizeMinusOne = maxSize - 1; u32 counts[128] = { 0 }; u32 highestHistoryCount = 0; for (u32 i=0; i!= sizeof_array(m_activeProcessCountHistory) - 1; ++i) highestHistoryCount = Max(highestHistoryCount, m_activeProcessCountHistory[i]); u32 activeProcessCount = highestHistoryCount; counts[0] = activeProcessCount; u32 totalHeight = counts[0]*2; while (totalHeight < maxHeight && counts[maxSizeMinusOne] != activeProcessCount) { bool changed = false; for (u32 i=0; i!=maxSizeMinusOne; ++i) { if (counts[i] && counts[i] > counts[i+1]*2 + 1) { --counts[i]; ++counts[i+1]; ++totalHeight; changed = true; } } if (!changed) { for (u32 j=0; j!=maxSizeMinusOne; ++j) { if (!counts[j]) continue; ++counts[j + 1]; --counts[j]; ++totalHeight; break; } } } auto it = activeProcesses.begin(); auto itEnd = activeProcesses.end(); int endY = int(startPosY + maxHeight); for (u32 i=0;i!=maxSize;++i) { u32 v = maxSizeMinusOne - i; u32 boxHeight = v + 1; for (u32 j=0;j!=counts[v];++j) { if (it == itEnd || posY >= endY) break; auto& active = *it->second; ++it; drawProcess(active, boxHeight, j == 0); posY += boxHeight + 1; } } //if (fillHeight) // posY = clientRect.bottom-1;//startPosY; // To prevent vertical scrollbar if (fillHeight || counts[maxSizeMinusOne] != activeProcessCount) posY = startPosY + maxHeight; else posY += 3; SetActiveFont(m_defaultFont); } void Visualizer::PaintProcessRect(TraceView::Process& process, HDC hdc, RECT rect, const RECT& progressRect, bool selected, bool writingBitmap, HBRUSH& lastSelectedBrush) { auto clampRect = [&](RECT& r) { r.left = Min(Max(r.left, progressRect.left), progressRect.right); r.right = Max(Min(r.right, progressRect.right), progressRect.left); }; bool done = process.stop != ~u64(0); HBRUSH brush = m_processBrushes[selected].success; if (!process.returnedReason.empty()) brush = m_processBrushes[selected].returned; else if (!done) brush = m_processBrushes[selected].inProgress; else if (process.cacheFetch) brush = m_processBrushes[selected].cacheFetch; else if (process.exitCode == ProcessCancelExitCode) brush = m_processBrushes[selected].returned; else if (process.exitCode != 0) brush = m_processBrushes[selected].error; u64 writeFilesTime = process.writeFilesTime; auto fillRect = [&](const RECT& r, HBRUSH b) { if (lastSelectedBrush != b) { SelectObject(hdc, b); lastSelectedBrush = b; } PatBlt(hdc, r.left, r.top, r.right - r.left, r.bottom - r.top, PATCOPY); }; if (!done || process.exitCode != 0 || !m_config.ShowReadWriteColors) { if (writingBitmap) rect.right = 256; clampRect(rect); fillRect(rect, brush); return; } double duration = double(process.stop - process.start); RECT main = rect; int width = rect.right - rect.left; double recvPart = (double(ConvertTime(m_traceView, process.createFilesTime)) / duration); if (int headSize = int(recvPart * width)) { main.left += headSize; RECT r2 = rect; r2.right = r2.left + headSize; clampRect(r2); if (r2.left != r2.right) fillRect(r2, m_processBrushes[selected].recv); } double sendPart = (double(ConvertTime(m_traceView, writeFilesTime)) / duration); if (int tailSize = int(sendPart * width)) { main.right -= tailSize; RECT r2 = rect; r2.left = r2.right - tailSize; clampRect(r2); if (r2.left != r2.right) fillRect(r2, m_processBrushes[selected].send); } clampRect(main); if (main.left != main.right) fillRect(main, brush); //clampRect(rect); /* if (!process.updates.empty()) { shouldDrawText = false; int prevUpdateX = rect.left; for (auto& update : process.updates) { int updateX = int(posX + TimeToS(update.time) * scaleX) - 1; RECT textRect{ prevUpdateX + 5, rect.top, updateX, rect.bottom }; DrawTextW(hdc, update.reason.c_str(), u32(update.reason.size()), &textRect, DT_SINGLELINE); SelectObject(hdc, m_processUpdatePen); MoveToEx(hdc, updateX, rect.top, NULL); LineTo(hdc, updateX, rect.bottom - 1); prevUpdateX = updateX; } } */ } void Visualizer::PaintTimeline(HDC hdc, const RECT& clientRect) { SetActiveFont(m_timelineFont); int top = GetTimelineTop(clientRect); float timeScale = (m_horizontalScaleValue*m_zoomValue)*50.0f; float startOffset = ((m_scrollPosX/timeScale) - float(int(m_scrollPosX/timeScale))) * timeScale; int index = -int(startOffset/timeScale); int number = -int(float(m_scrollPosX)/timeScale); int textStepSize = int((5.0f / timeScale) + 1) * 5; if (textStepSize > 150) textStepSize = 600; else if (textStepSize > 120) textStepSize = 300; else if (textStepSize > 90) textStepSize = 240; else if (textStepSize > 45) textStepSize = 120; else if (textStepSize > 30) textStepSize = 60; else if (textStepSize > 10) textStepSize = 30; int lineStepSize = textStepSize / 5; RECT progressRect = clientRect; progressRect.left += m_progressRectLeft; SelectObject(hdc, m_textPen); Vector lines; Vector points; while (true) { int pos = progressRect.left + int(startOffset + float(index)*timeScale); if (pos >= clientRect.right) break; int lineBottom = top + 5; if (!(number % textStepSize)) { bool shouldDraw = true; int seconds = number; StringBuffer<> buffer; if (seconds >= 60) { int min = seconds / 60; seconds -= min * 60; if (!seconds) { buffer.Appendf(L"%um", min); lineBottom += 4; } } if (!number || seconds) buffer.Appendf(L"%u", seconds); if (shouldDraw) { SIZE s; GetTextExtentPoint32W(hdc, buffer.data, buffer.count, &s); RECT textRect; textRect.top = top + 8; textRect.bottom = textRect.top + m_activeFont.height; textRect.right = pos + s.cx/2; textRect.left = pos - s.cx/2; ExtTextOutW(hdc, textRect.left, textRect.top, 0, NULL, buffer.data, buffer.count, NULL); } } if (!(number % lineStepSize)) { points.push_back({pos, top}); points.push_back({pos, lineBottom}); lines.push_back(2); } ++number; ++index; } points.push_back({m_contentWidth, top - 25}); points.push_back({m_contentWidth, top}); lines.push_back(2); PolyPolyline(hdc, points.data(), lines.data(), int(lines.size())); /* POINT cursorPos; GetCursorPos(&cursorPos); ScreenToClient(m_hwnd, &cursorPos); RECT lineRect; lineRect.left = cursorPos.x; lineRect.right= cursorPos.x + 1; lineRect.top = top; lineRect.bottom = top + 15; FillRect(hdc, &lineRect, m_lineBrush); */ } void Visualizer::PaintDetailedStats(int& posY, const RECT& progressRect, TraceView::Session& session, bool isRemote, u64 playTime, const DrawTextFunc& drawTextFunc) { int stepY = m_activeFont.height; int startPosY = posY; int posX = progressRect.left + 5; int maxY = posY + stepY; u32 maxTextWidth = 0; RECT textRect; auto drawText = [&](const wchar_t* format, ...) { textRect.top = posY; textRect.bottom = posY + 20; textRect.left = posX; textRect.right = posX + 1000; posY += stepY; StringBuffer<> str; va_list arg; va_start(arg, format); str.Append(format, arg); va_end(arg); u32 lastTextWidth = 0; drawTextFunc(str, textRect, &lastTextWidth); maxTextWidth = Max(maxTextWidth, lastTextWidth); maxY = Max(maxY, posY); }; if (isRemote) { drawText(L"Finished Processes: %u", session.processExitedCount); drawText(L"Active Processes: %u", session.processActiveCount); drawText(L"ClientId: %u TcpCount: %u", session.clientUid.data1, session.connectionCount.empty() ? 1 : session.connectionCount.back()); if (session.disconnectTime == ~u64(0)) { if (session.proxyCreated) drawText(L"Proxy(HOSTED): %ls", session.proxyName.c_str()); else if (!session.proxyName.empty()) drawText(L"Proxy: %ls", session.proxyName.c_str()); else drawText(L"Proxy: None"); } bool hasFileDetails = !session.fetchedFiles.empty() || !session.storedFiles.empty(); if (!hasFileDetails) { posY = startPosY; posX += maxTextWidth + 15; } if (!session.updates.empty()) { auto updateTime = session.updates.back(); auto updateSend = session.networkSend.back(); auto updateRecv = session.networkRecv.back(); u64 sendPerS = 0; u64 recvPerS = 0; float duration = TimeToS(updateTime - session.prevUpdateTime); if (duration != 0) { sendPerS = u64(float(updateSend - session.prevSend) / duration); recvPerS = u64(float(updateRecv - session.prevRecv) / duration); } drawText(L"Recv: %ls (%sit/s)", BytesToText(updateRecv), BytesToText(recvPerS*8)); drawText(L"Send: %ls (%sit/s)", BytesToText(updateSend), BytesToText(sendPerS*8)); } int fileWidth = 700; auto drawFiles = [&](const wchar_t* fileType, Vector& files, u64 count, u64 bytes, u64 activeCount, u32& maxVisibleFiles) { textRect.left = posX; textRect.right = posX + fileWidth; drawText(L"%ls Files: %u (%u) %s", fileType, u32(count), u32(activeCount), BytesToText(bytes)); u32 fileCount = 0; for (auto rit = files.rbegin(), rend = files.rend(); rit != rend; ++rit) { TraceView::FileTransfer& file = *rit; if (file.stop != ~u64(0)) continue; u64 time = 0; if (file.start < playTime) time = playTime - file.start; if ((((u8*)&file.key)[19] & 4) != 0) drawText(L"%s (proxy) %s", file.hint.c_str(), TimeToText(time, true).str); else if (file.size == 0) drawText(L"%s (calc) %s", file.hint.c_str(), TimeToText(time, true).str); else drawText(L"%s (%s) %s", file.hint.c_str(), BytesToText(file.size).str, TimeToText(time, true).str); if (fileCount++ > 5) break; } posY += stepY * (maxVisibleFiles - fileCount); maxVisibleFiles = Max(maxVisibleFiles, fileCount); }; Vector fetchedFiles; for (auto& kv : session.fetchedFilesActive) fetchedFiles.push_back(session.fetchedFiles[kv.second]); std::sort(fetchedFiles.begin(), fetchedFiles.end(), [](const TraceView::FileTransfer& a, const TraceView::FileTransfer& b) { return a.start > b.start; }); if (hasFileDetails) { posY = startPosY; posX += maxTextWidth + 15; } drawFiles(L"Fetched", fetchedFiles, session.fetchedFilesCount, session.fetchedFilesBytes, session.fetchedFilesActive.size(), session.maxVisibleFiles); if (hasFileDetails) { posY = startPosY; posX += fileWidth; } drawFiles(L"Stored", session.storedFiles, session.storedFilesCount, session.storedFilesBytes, session.storedFilesActive.size(), session.maxVisibleFiles); } else { drawText(L"Finished Processes: %u (local: %u)", m_traceView.totalProcessExitedCount, session.processExitedCount); drawText(L"Active Processes: %u (local: %u)", m_traceView.totalProcessActiveCount, session.processActiveCount); drawText(L"Active Helpers: %u", Max(1u, m_traceView.activeSessionCount) - 1); if (session.highestSendPerS || session.highestRecvPerS) { auto updateTime = session.updates.back(); auto updateSend = session.networkSend.back(); auto updateRecv = session.networkRecv.back(); if (updateSend || updateRecv) { u64 sendPerS = 0; u64 recvPerS = 0; float duration = TimeToS(updateTime - session.prevUpdateTime); if (duration != 0) { sendPerS = u64(float(updateSend - session.prevSend) / duration); recvPerS = u64(float(updateRecv - session.prevRecv) / duration); } drawText(L"Recv: %ls (%sit/s)", BytesToText(updateRecv), BytesToText(recvPerS)); drawText(L"Send: %ls (%sit/s)", BytesToText(updateSend), BytesToText(sendPerS)); } } if (!session.updates.empty()) { posY = startPosY; posX += maxTextWidth + 10; for (auto& pair : session.drives) { auto& drive = pair.second; u64 readPerS = 0; u64 writePerS = 0; u64 updateCount = session.updates.size(); float duration = TimeToS(session.updates.back() - session.prevUpdateTime); if (duration != 0 && updateCount > 1) { readPerS = u64(float(drive.readBytes.back()) / duration); writePerS = u64(float(drive.writeBytes.back()) / duration); } drawText(L"%c: Rd %ls (%s/s) Wr %ls (%s/s) ", pair.first, BytesToText(drive.totalReadBytes).str, BytesToText(readPerS).str, BytesToText(drive.totalWriteBytes).str, BytesToText(writePerS).str); } } } posY = maxY; } u64 Visualizer::GetPlayTime() { u64 currentTime = m_paused ? m_pauseStart : GetTime(); u64 playTime = 0; if (m_traceView.startTime && currentTime > (m_traceView.startTime + m_pauseTime)) playTime = currentTime - m_traceView.startTime - m_pauseTime; if (m_replay) playTime *= m_replay; return playTime; } int Visualizer::GetTimelineHeight() { return m_timelineFont.height + 8; } int Visualizer::GetTimelineTop(const RECT& clientRect) { int timelineHeight = GetTimelineHeight(); int posY = m_contentHeight - timelineHeight; int maxY = int(clientRect.bottom - timelineHeight); return m_config.LockTimelineToBottom ? maxY : Min(posY, maxY); } void Visualizer::HitTest(HitTestResult& outResult, const POINT& pos) { SetActiveFont(m_defaultFont); u64 playTime = GetPlayTime(); RECT clientRect; GetClientRect(m_hwnd, &clientRect); int posY = int(m_scrollPosY); int boxHeight = m_boxHeight; int processStepY = boxHeight + 1; float scaleX = 50.0f*m_zoomValue*m_horizontalScaleValue; RECT progressRect = clientRect; progressRect.left += m_progressRectLeft; progressRect.bottom -= 30; { int boxSide = 8; int boxStride = boxSide + 2; int top = 5; int bottom = top + boxSide; int left = progressRect.right - 7 - boxSide; int right = progressRect.right - 7; for (int i = VisualizerFlag_Count - 1; i >= 0; --i) { if (pos.x >= left && pos.x <= right && pos.y >= top && pos.y <= bottom) { outResult.buttonSelected = i; return; } left -= boxStride; right -= boxStride; } } outResult.section = 0; if (m_config.showTimeline && !m_traceView.sessions.empty()) { int timelineTop = GetTimelineTop(clientRect); if (pos.y >= timelineTop)// && pos.y < timelineTop + 40) { outResult.section = 3; float timeScale = (m_horizontalScaleValue * m_zoomValue)*50.0f; float startOffset = -(m_scrollPosX / timeScale); outResult.timelineSelected = startOffset + float(pos.x - m_progressRectLeft) / timeScale; return; } } u64 lastStop = 0; if (m_config.showProgress && m_traceView.progressProcessesTotal) posY += m_activeFont.height + 2; if (m_config.showStatus && !m_traceView.statusMap.empty()) { u32 lastRow = ~0u; u32 row = ~0u; for (auto& kv : m_traceView.statusMap) { if (kv.second.text.empty()) continue; row = u32(kv.first >> 32); if (lastRow != ~0u && lastRow != row) posY += m_activeFont.height + 2; lastRow = row; if (!kv.second.link.empty()) { if (pos.y >= posY && pos.y < posY + m_activeFont.height && pos.x > 20 && pos.x < 80) // TODO: x is just hard coded to fit horde for now { outResult.hyperLink = kv.second.link; return; } } } if (row != ~0u) posY += m_activeFont.height + 2; posY += 3; } if (pos.y < posY) return; outResult.section = 1; if (m_config.showActiveProcessGraph) { if (pos.y > posY && pos.y < posY + GraphHeight) { float timeScale = (m_horizontalScaleValue * m_zoomValue)*50.0f; float startOffset = -(m_scrollPosX / timeScale); u64 selectedTimeMs = u64(1000.0f*(startOffset + float(pos.x - m_progressRectLeft) / timeScale)); u64 lastTime = m_traceView.activeProcessCounts.empty() ? 0 : m_traceView.activeProcessCounts[m_traceView.activeProcessCounts.size()-1].time; if (selectedTimeMs < TimeToMs(lastTime)) { u16 count = 0; for (auto& entry : m_traceView.activeProcessCounts) { count = entry.count; if (TimeToMs(entry.time) > selectedTimeMs) break; } outResult.activeProcessGraphSelected = true; outResult.activeProcessCount = count; } } posY += GraphHeight; } TraceView::ProcessLocation& outLocation = outResult.processLocation; if (m_config.showActiveProcesses && !m_trace.m_activeProcesses.empty()) { PaintActiveProcesses(posY, clientRect, [&](TraceView::ProcessLocation& processLocation, u32 boxHeight, bool firstWithHeight) { if (pos.y < posY || pos.y > posY + int(boxHeight)) return; auto& session = m_trace.GetSession(m_traceView, processLocation.sessionIndex); TraceView::Process& process = session.processors[processLocation.processorIndex].processes[processLocation.processIndex]; int posX = int(m_scrollPosX) + progressRect.left; u64 stop = process.stop; bool done = stop != ~u64(0); if (!done) stop = playTime; int left = posX + int(float(TimeToS(process.start)) * scaleX); int right = posX + int(float(TimeToS(stop)) * scaleX) - 1; if (pos.x >= left && pos.x <= right) { outLocation.sessionIndex = processLocation.sessionIndex; outLocation.processorIndex = processLocation.processorIndex; outLocation.processIndex = processLocation.processIndex; outResult.processSelected = true; return; } }); if (outResult.processSelected) return; } if (pos.y < posY) return; outResult.section = 2; SessionRec sortedSessions[1024]; Populate(sortedSessions, m_traceView, m_config.SortActiveRemoteSessions); for (u64 sessionIt = 0, sessionEnd = m_traceView.sessions.size(); sessionIt != sessionEnd; ++sessionIt) { bool isFirst = sessionIt == 0; auto& session = *sortedSessions[sessionIt].session; bool hasUpdates = !session.updates.empty(); if (!isFirst) { if (!hasUpdates && session.processors.empty()) continue; if (!m_config.showFinishedProcesses && session.disconnectTime != ~u64(0)) continue; } u32 sessionIndex = sortedSessions[sessionIt].index; if (!isFirst) posY += 3; if (m_config.showTitleBars) { if (pos.y >= posY && pos.y < posY + m_sessionStepY) { if (pos.x < int(session.fullNameWidth) + 5) { outResult.sessionSelectedIndex = sessionIndex; return; } } posY += m_sessionStepY; } bool showGraph = m_config.showNetworkStats || m_config.showCpuMemStats || m_config.showDriveStats; if (showGraph && !session.updates.empty()) { if (pos.y >= posY && pos.y < posY + GraphHeight) { int posX = int(m_scrollPosX) + progressRect.left; bool loop = true; u32 reconnectIndex = 0; while (loop) { u64 i = 0; u64 e = 0; if (reconnectIndex) i = session.reconnectIndices[reconnectIndex - 1]; if (reconnectIndex < session.reconnectIndices.size()) e = session.reconnectIndices[reconnectIndex]; else { e = session.updates.size(); loop = false; } int prevX = 100000; for (; i!=e; ++i) { u64 updateTime = session.updates[i]; auto updateSend = session.networkSend[i]; auto updateRecv = session.networkRecv[i]; int x = posX + int(float(TimeToS(updateTime)) * scaleX); int hitOffset = (prevX - x)/2; if (pos.x + hitOffset >= prevX && pos.x + hitOffset <= x) { u64 prevTime = 0; u64 prevSend = 0; u64 prevRecv = 0; if (i > 0) { prevTime = session.updates[i - 1]; prevSend = Min(session.networkSend[i - 1], updateSend); prevRecv = Min(session.networkRecv[i - 1], updateRecv); } double duration = TimeToS(updateTime - prevTime); outResult.stats.recvBytes = updateRecv; outResult.stats.sendBytes = updateSend; outResult.stats.recvBytesPerSecond = u64(float(updateRecv - prevRecv) / duration); outResult.stats.sendBytesPerSecond = u64(float(updateSend - prevSend) / duration); outResult.stats.ping = session.ping[i]; outResult.stats.memAvail = session.memAvail[i]; outResult.stats.cpuLoad = session.cpuLoad[i]; outResult.stats.memTotal = session.memTotal; outResult.statsSelected = true; for (auto& pair : session.drives) { auto& updateBusy = pair.second.busyPercent[i]; u64 updateRead = pair.second.readBytes[i]; u64 updateWrite = pair.second.writeBytes[i]; auto& statsDrive = outResult.stats.drives[pair.first]; statsDrive.busyPercent = updateBusy; statsDrive.readPerSecond = u64(float(updateRead) / duration); statsDrive.writePerSecond = u64(float(updateWrite) / duration); } return; } prevX = x; } ++reconnectIndex; } posY += GraphHeight; } posY += GraphHeight; } if (m_config.showDetailedData) { auto drawText = [&](const StringBufferBase& text, RECT& rect, u32* outWidth) { if (pos.x >= rect.left && pos.x < rect.right && pos.y >= rect.top && pos.y < rect.bottom && text.StartsWith(TC("Fetched Files"))) outResult.fetchedFilesSelected = sessionIndex; }; PaintDetailedStats(posY, progressRect, session, sessionIt != 0, playTime, drawText); } if (m_config.showProcessBars) { u32 processorIndex = 0; for (auto& processor : session.processors) { bool drawProcessorIndex = m_config.showFinishedProcesses; if (pos.y < progressRect.bottom && posY + processStepY >= progressRect.top && posY <= progressRect.bottom && pos.y >= posY-1 && pos.y < posY-1 + processStepY) { u32 processIndex = 0; int posX = int(m_scrollPosX) + progressRect.left; for (auto& process : processor.processes) { int left = posX + int(float(TimeToS(process.start)) * scaleX); auto pig = MakeGuard([&]() { ++processIndex; }); if (left >= progressRect.right) continue; if (left < progressRect.left) left = progressRect.left; u64 stopTime = process.stop; bool done = stopTime != ~u64(0); if (!done) stopTime = playTime; else if (!m_config.showFinishedProcesses) continue; drawProcessorIndex = true; RECT rect; rect.left = left; rect.right = posX + int(float(TimeToS(stopTime)) * scaleX); if (rect.right <= progressRect.left) continue; if (!m_filterString.empty() && !Contains(process.description.c_str(), m_filterString.c_str()) && !Contains(process.breadcrumbs.c_str(), m_filterString.c_str())) continue; rect.right = Max(int(rect.right), left + 1); rect.top = posY; rect.bottom = posY + int(float(18) * m_zoomValue); if (pos.x >= rect.left && pos.x <= rect.right) { outLocation.sessionIndex = sessionIndex; outLocation.processorIndex = processorIndex; outLocation.processIndex = processIndex; outResult.processSelected = true; return; } } } if (!processor.processes.empty()) lastStop = Max(lastStop, processor.processes.rbegin()->stop); if (drawProcessorIndex) posY += processStepY; ++processorIndex; } } else { for (auto& processor : session.processors) if (!processor.processes.empty()) lastStop = Max(lastStop, processor.processes.rbegin()->stop); } if (m_config.showWorkers && isFirst) { int trackIndex = 0; for (auto& workTrack : m_traceView.workTracks) { if (pos.y < progressRect.bottom && posY + processStepY >= progressRect.top && posY <= progressRect.bottom && pos.y >= posY-1 && pos.y < posY-1 + processStepY) { u32 workIndex = 0; int posX = int(m_scrollPosX) + progressRect.left; for (auto& work : workTrack.records) { auto inc = MakeGuard([&](){ ++workIndex; }); int left = posX + int(float(TimeToS(work.start)) * scaleX); if (left >= progressRect.right) continue; if (!m_filterString.empty()) { bool keep = Contains(work.description, m_filterString.c_str()); if (!keep) for (auto& en : work.entries) keep |= Contains(en.text, m_filterString.c_str()); if (!keep) continue; } if (left < progressRect.left) left = progressRect.left; u64 stopTime = work.stop; bool done = stopTime != ~u64(0); if (!done) stopTime = playTime; RECT rect; rect.left = left; rect.right = posX + int(float(TimeToS(stopTime)) * scaleX); if (rect.right <= progressRect.left) continue; rect.right = Max(int(rect.right), left + 1); rect.top = posY; rect.bottom = posY + int(float(18) * m_zoomValue); if (pos.x >= rect.left && pos.x <= rect.right) { outResult.workTrack = trackIndex; outResult.workIndex = workIndex; outResult.workSelected = true; return; } } } ++trackIndex; posY += processStepY; } } } m_contentWidth = m_progressRectLeft + Max(0, int(TimeToS((lastStop != 0 && lastStop != ~u64(0)) ? lastStop : playTime) * scaleX)); m_contentHeight = posY - int(m_scrollPosY) + processStepY + 14; } void Visualizer::WriteProcessStats(Logger& out, const TraceView::Process& process) { bool hasExited = process.stop != ~u64(0); out.Info(L" %ls", process.description.c_str()); out.Info(L" ProcessId: %u", process.id); out.Info(L" Start: %ls", TimeToText(process.start, true).str); if (hasExited) out.Info(L" Duration: %ls", TimeToText(process.stop - process.start, true).str); if (hasExited && process.exitCode != 0) out.Info(L" ExitCode: %u", process.exitCode); if (process.stop != ~u64(0)) { out.Info(L""); BinaryReader reader(process.stats.data(), 0, process.stats.size()); ProcessStats processStats; SessionStats sessionStats; StorageStats storageStats; KernelStats kernelStats; processStats.Read(reader, m_traceView.version); if (reader.GetLeft()) { if (process.isRemote || (m_traceView.version >= 36 && !process.isReuse)) sessionStats.Read(reader, m_traceView.version); storageStats.Read(reader, m_traceView.version); kernelStats.Read(reader, m_traceView.version); } out.Info(L" ----------- Detours stats -----------"); processStats.Print(out, m_traceView.frequency); if (!sessionStats.IsEmpty()) { out.Info(L""); out.Info(L" ----------- Session stats -----------"); sessionStats.Print(out, m_traceView.frequency); } if (!storageStats.IsEmpty()) { out.Info(L""); out.Info(L" ----------- Storage stats -----------"); storageStats.Print(out, m_traceView.frequency); } if (!kernelStats.IsEmpty()) { out.Info(L""); out.Info(L" ----------- Kernel stats ------------"); kernelStats.Print(out, false, m_traceView.frequency); } PrintCacheWriteStats(out, process.id); } } void Visualizer::WriteWorkStats(Logger& out, const TraceView::WorkRecord& record) { out.Info(L" %ls", record.description); out.Info(L" Start: %ls", TimeToText(record.start, true).str); if (record.stop != ~u64(0)) out.Info(L" Duration: %ls", TimeToText(record.stop - record.start, true).str); for (auto& e : record.entries) out.Info(L" %ls (%ls)", e.text, TimeToText(e.time - record.start).str); } void Visualizer::CopyTextToClipboard(const TString& str) { if (!OpenClipboard(m_hwnd)) return; if (auto hglbCopy = GlobalAlloc(GMEM_MOVEABLE, (str.size() + 1) * sizeof(TCHAR))) { if (auto lptstrCopy = GlobalLock(hglbCopy)) { memcpy(lptstrCopy, str.data(), (str.size() + 1) * sizeof(TCHAR)); GlobalUnlock(hglbCopy); EmptyClipboard(); SetClipboardData(CF_UNICODETEXT, hglbCopy); } } CloseClipboard(); } void Visualizer::UnselectAndRedraw() { if (Unselect() || m_config.showCursorLine) Redraw(false); } bool Visualizer::UpdateAutoscroll() { if (!m_autoScroll) return false; u64 playTime = GetPlayTime(); RECT rect; GetClientRect(m_hwnd, &rect); if (rect.right == 0) return false; float timeS = TimeToS(playTime); if (m_config.AutoScaleHorizontal) { m_scrollPosX = 0; timeS = Max(timeS, 20.0f/m_zoomValue); m_horizontalScaleValue = Max(float(rect.right - m_progressRectLeft - 2)/(m_zoomValue*timeS*50.0f), 0.001f); return true; } else { float oldScrollPosX = m_scrollPosX; m_scrollPosX = Min(0.0f, (float)rect.right - timeS*50.0f*m_horizontalScaleValue*m_zoomValue - float(m_progressRectLeft)); return oldScrollPosX != m_scrollPosX; } } bool Visualizer::UpdateSelection() { if (!m_mouseOverWindow || m_dragToScrollCounter > 0) return false; POINT pos; GetCursorPos(&pos); ScreenToClient(m_hwnd, &pos); HitTestResult res; HitTest(res, pos); m_activeSection = res.section; if (res.processSelected == m_processSelected && res.processLocation == m_processSelectedLocation && res.sessionSelectedIndex == m_sessionSelectedIndex && res.statsSelected == m_statsSelected && memcmp(&res.stats, &m_stats, sizeof(Stats)) == 0 && res.buttonSelected == m_buttonSelected && res.timelineSelected == m_timelineSelected && res.activeProcessGraphSelected == m_activeProcessGraphSelected && res.activeProcessCount == m_activeProcessCount && res.fetchedFilesSelected == m_fetchedFilesSelected && res.workSelected == m_workSelected && res.workTrack == m_workTrack && res.workIndex == m_workIndex && res.hyperLink == m_hyperLinkSelected) return false; m_processSelected = res.processSelected; m_processSelectedLocation = res.processLocation; m_sessionSelectedIndex = res.sessionSelectedIndex; m_statsSelected = res.statsSelected; m_stats = res.stats; m_activeProcessGraphSelected = res.activeProcessGraphSelected; m_activeProcessCount = res.activeProcessCount; m_buttonSelected = res.buttonSelected; m_timelineSelected = res.timelineSelected; m_fetchedFilesSelected = res.fetchedFilesSelected; m_workSelected = res.workSelected; m_workTrack = res.workTrack; m_workIndex = res.workIndex; m_hyperLinkSelected = res.hyperLink; return true; } void Visualizer::UpdateScrollbars(bool redraw) { RECT rect; GetClientRect(m_hwnd, &rect); SCROLLINFO si; si.cbSize = sizeof(SCROLLINFO); si.fMask = SIF_ALL | SIF_DISABLENOSCROLL; si.nMin = 0; si.nMax = m_contentHeight; si.nPage = int(rect.bottom); si.nPos = -int(m_scrollPosY); si.nTrackPos = 0; bool updateFrame = false; if (ActiveProcessesShouldFillHeight()) { if (m_verticalScrollBarEnabled) { SetWindowLong(m_hwnd, GWL_STYLE, GetWindowLong(m_hwnd, GWL_STYLE) & ~WS_VSCROLL); m_verticalScrollBarEnabled = false; updateFrame = true; } } else { if (!m_verticalScrollBarEnabled) { SetWindowLong(m_hwnd, GWL_STYLE, GetWindowLong(m_hwnd, GWL_STYLE) | WS_VSCROLL); m_verticalScrollBarEnabled = true; updateFrame = true; } SetScrollInfo(m_hwnd, SB_VERT, &si, redraw); } if (m_config.AutoScaleHorizontal) { if (m_horizontalScrollBarEnabled) { SetWindowLong(m_hwnd, GWL_STYLE, GetWindowLong(m_hwnd, GWL_STYLE) & ~WS_HSCROLL); m_horizontalScrollBarEnabled = false; updateFrame = true; } } else { if (!m_horizontalScrollBarEnabled) { SetWindowLong(m_hwnd, GWL_STYLE, GetWindowLong(m_hwnd, GWL_STYLE) | WS_HSCROLL); m_horizontalScrollBarEnabled = true; updateFrame = true; } si.nMax = m_contentWidth; si.nPage = rect.right; si.nPos = -int(m_scrollPosX); SetScrollInfo(m_hwnd, SB_HORZ, &si, redraw); } if (updateFrame) SetWindowPos(m_hwnd, NULL, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED); } void Visualizer::SetActiveFont(const Font& font) { m_activeFont = font; if (m_activeHdc) SelectObject(m_activeHdc, font.handle); } void Visualizer::UpdateTheme() { SetWindowTheme(m_hwnd, m_config.DarkMode ? L"DarkMode_Explorer" : L"Explorer", NULL); SendMessageW(m_hwnd, WM_THEMECHANGED, 0, 0); BOOL useDarkMode = m_config.DarkMode; u32 attribute = 20; // DWMWA_USE_IMMERSIVE_DARK_MODE DwmSetWindowAttribute(m_hwnd, attribute, &useDarkMode, sizeof(useDarkMode)); } bool Visualizer::ActiveProcessesShouldFillHeight() { return !m_config.showDetailedData && !m_config.showTitleBars && !m_config.showCpuMemStats && !m_config.showNetworkStats && !m_config.showDriveStats && !m_config.showProcessBars; } StringBuffer<128> Visualizer::GetWorldTime(u64 time) { return GetWorldTime(TimeToS(time)); } StringBuffer<128> Visualizer::GetWorldTime(float seconds) { time_t rawTime = m_traceView.traceSystemStartTimeUs / 1'000'000; rawTime += (time_t)seconds; struct tm timeinfo; localtime_s(&timeinfo, &rawTime); StringBuffer<128> buffer; buffer.count = u32(wcsftime(buffer.data, buffer.capacity, L"%Y-%m-%d %H:%M:%S", &timeinfo)); return buffer; } void Visualizer::PostNewTrace(u32 replay, bool paused) { KillTimer(m_hwnd, 0); PostMessage(m_hwnd, WM_NEWTRACE, replay, paused); } void Visualizer::PostNewTitle(const StringView& title) { #if !defined( __clang_analyzer__ ) PostMessage(m_hwnd, WM_SETTITLE, 0, (LPARAM)_wcsdup(title.data)); #endif } void Visualizer::PostQuit() { m_looping = false; PostMessage(m_hwnd, WM_USER+666, 0, 0); } LRESULT Visualizer::WinProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { switch (Msg) { case WM_SETTITLE: { auto title = (wchar_t*)lParam; SetWindowTextW(hWnd, title); free(title); break; } case WM_NEWTRACE: { m_replay = u32(wParam); m_paused = lParam; m_autoScroll = true; m_scrollPosX = 0; m_scrollPosY = 0; Reset(); StringBuffer<> title; GetTitlePrefix(title); auto g = MakeGuard([&]() { Redraw(true); UpdateScrollbars(true); }); if (m_client) { if (!m_trace.StartReadClient(m_traceView, *m_client)) { m_clientDisconnect.Set(); return false; } m_namedTrace.Clear().Append(m_newTraceName); m_traceView.finished = false; } else if (!m_fileName.IsEmpty()) { m_trace.ReadFile(m_traceView, m_fileName.data, m_replay != 0); m_traceView.finished = m_replay == 0; PostNewTitle(GetTitlePrefix(title).Appendf(L"%s (v%u) - %s", m_fileName.data, m_traceView.version, GetWorldTime((u64)0).data)); } else if (m_usingNamed) { if (!m_trace.StartReadNamed(m_traceView, m_newTraceName.data, true, m_replay != 0)) return false; m_namedTrace.Clear().Append(m_newTraceName); m_traceView.finished = false; PostNewTitle(GetTitlePrefix(title).Appendf(L"%s (Listening for new sessions on channel '%s')", m_namedTrace.data, m_listenChannel.data)); } SetTimer(m_hwnd, 0, 200, NULL); return 0; } case WM_SYSCOMMAND: // Don't send this message through DefWindowProc as it will destroy the window // and GetMessage will stay stuck indefinitely. if (wParam == SC_CLOSE) { PostQuit(); return 0; } break; case WM_DESTROY: PostQuit(); return 0; case WM_ERASEBKGND: return 1; case WM_PAINT: { u64 start = GetTime(); PaintClient([&](HDC hdc, HDC memDC, RECT& rect) { FillRect(memDC, &rect, m_backgroundBrush); m_activeHdc = memDC; PaintAll(memDC, rect); m_activeHdc = 0; BitBlt(hdc, 0, 0, rect.right - rect.left, rect.bottom - rect.top, memDC, 0, 0, SRCCOPY); }); m_lastPaintTimeMs = TimeToMs(GetTime() - start); break; } case WM_SIZE: { int height = HIWORD(lParam); if (m_contentHeight && m_contentHeight + int(m_scrollPosY) < height) m_scrollPosY = float(Min(0, height - m_contentHeight)); int width = LOWORD(lParam); if (m_contentWidth && m_contentWidth + int(m_scrollPosX) < width) m_scrollPosX = float(Min(0, width - m_contentWidth)); UpdateScrollbars(true); break; } case WM_TIMER: { bool changed = false; if (!m_paused) { u64 timeOffset = (GetTime() - m_startTime - m_pauseTime) * m_replay; if (!m_fileName.IsEmpty()) { if (m_replay) m_trace.UpdateReadFile(m_traceView, timeOffset, changed); } else if (m_client) { if (!m_trace.UpdateReadClient(m_traceView, *m_client, changed)) m_clientDisconnect.Set(); } else if (m_usingNamed) { if (!m_trace.UpdateReadNamed(m_traceView, m_replay ? timeOffset : ~u64(0), changed)) if (m_listenTimeout.IsCreated()) m_listenTimeout.Set(); } } if (m_traceView.finished) { m_autoScroll = false; KillTimer(m_hwnd, 0); changed = true; } changed = UpdateAutoscroll() || changed; changed = UpdateSelection() || changed; if (changed && !IsIconic(m_hwnd)) { UpdateScrollbars(true); RedrawWindow(m_hwnd, NULL, NULL, RDW_INVALIDATE); // Don't use RDW_UPDATENOW.. it will cause stutter u32 waitTime = 60u;//u32(Min(m_lastPaintTimeMs * 5, 200ull)); if (!m_traceView.finished) SetTimer(m_hwnd, 0, waitTime, NULL); } break; } case WM_MOUSEWHEEL: { if (m_dragToScrollCounter > 0) break; int delta = GET_WHEEL_DELTA_WPARAM(wParam); bool controlDown = GetAsyncKeyState(VK_CONTROL) & (1<<15); bool shiftDown = GetAsyncKeyState(VK_LSHIFT) & (1<<15); if (m_config.ScaleHorizontalWithScrollWheel || controlDown || shiftDown) { if (m_activeSection == 2 || !controlDown) // process bars { RECT r; GetClientRect(hWnd, &r); // Use mouse cursor as scroll anchor point POINT cursorPos = {}; GetCursorPos(&cursorPos); ScreenToClient(m_hwnd, &cursorPos); float newScaleValue = m_horizontalScaleValue; int newBoxHeight = m_boxHeight; if (controlDown) { if (delta < 0) { if (newBoxHeight > 1) --newBoxHeight; } else if (delta > 0) ++newBoxHeight; } else newScaleValue = Max(m_horizontalScaleValue + m_horizontalScaleValue*float(delta)*0.0006f, 0.001f); // TODO: m_progressRectLeft changes with zoom so anchor logic is wrong const float scrollAnchorOffsetX = float(cursorPos.x) - float(m_progressRectLeft); const float scrollAnchorOffsetY = 0;//float(cursorPos.y)*m_zoomValue;// - m_progressRectLeft; float oldZoomValue = m_zoomValue; if (newBoxHeight != m_boxHeight) { m_boxHeight = newBoxHeight; UpdateProcessFont(); } m_scrollPosY = Min(0.0f, float(m_scrollPosY - scrollAnchorOffsetY)*(m_zoomValue/oldZoomValue) + scrollAnchorOffsetY); m_scrollPosX = Min(0.0f, float(m_scrollPosX - scrollAnchorOffsetX)*(m_zoomValue/oldZoomValue)*(newScaleValue/m_horizontalScaleValue) + scrollAnchorOffsetX);//LOWORD(lParam); if (m_horizontalScaleValue != newScaleValue) m_horizontalScaleValue = newScaleValue; UpdateAutoscroll(); UpdateSelection(); int minScroll = r.right - m_contentWidth; m_scrollPosX = Min(0.0f, Max(m_scrollPosX, float(minScroll))); m_scrollPosY = Min(0.0f, Max(m_scrollPosY, float(r.bottom - m_contentHeight))); //if (!m_traceView.finished && m_scrollPosX <= minScroll) // m_autoScroll = true; if (m_config.ShowReadWriteColors) for (auto& session : m_traceView.sessions) for (auto& processor : session.processors) for (auto& process : processor.processes) //if (TimeToMs(process.writeFilesTime, m_traceView.frequency) >= 300 || TimeToMs(process.createFilesTime, m_traceView.frequency) >= 300) process.bitmapDirty = true; } else if (m_activeSection == 1) // active processes { if (delta < 0) m_config.maxActiveProcessHeight = Max(m_config.maxActiveProcessHeight - 1u, 5u); else if (delta > 0) m_config.maxActiveProcessHeight = Min(m_config.maxActiveProcessHeight + 1u, 32u); } else if (m_activeSection == 0 || m_activeSection == 3) // status/timeline { if (delta < 0) m_config.fontSize -= 1; else if (delta > 0) m_config.fontSize += 1; UpdateDefaultFont(); } UpdateScrollbars(true); Redraw(false); } else { RECT r; GetClientRect(hWnd, &r); float oldScrollY = m_scrollPosY; m_scrollPosY = m_scrollPosY + float(delta); m_scrollPosY = Min(Max(m_scrollPosY, float(r.bottom - m_contentHeight)), 0.0f); if (oldScrollY != m_scrollPosY) { UpdateScrollbars(true); Redraw(false); } } break; } case WM_NCHITTEST: if (m_parentHwnd) return HTCLIENT; break; case WM_MOUSEMOVE: { //m_logger.Info(TC("Section: %u"), m_activeSection); POINTS p = MAKEPOINTS(lParam); POINT pos{ p.x, p.y }; if (m_dragToScrollCounter > 0) { RECT r; GetClientRect(hWnd, &r); if (m_contentHeight <= r.bottom) m_scrollPosY = 0; else m_scrollPosY = Max(Min(m_scrollAtAnchorY + float(pos.y - m_mouseAnchor.y), 0.0f), float(r.bottom - m_contentHeight)); if (m_contentWidth <= r.right) m_scrollPosX = 0; else { int minScroll = r.right - m_contentWidth; m_scrollPosX = Max(Min(m_scrollAtAnchorX + float(pos.x - m_mouseAnchor.x), 0.0f), float(minScroll)); if (!m_traceView.finished && m_scrollPosX <= float(minScroll)) m_autoScroll = true; } UpdateScrollbars(true); Redraw(false); } else { if (UpdateSelection() || m_config.showCursorLine) Redraw(false); /* else { PaintClient([&](HDC hdc, HDC memDC, RECT& rect) { RECT timelineRect = rect; rect.top = Min(rect.bottom, m_contentHeight) - 28; rect.bottom = Min(rect.bottom, rect.top + 28); FillRect(memDC, &rect, m_backgroundBrush); SetBkMode(memDC, TRANSPARENT); SetTextColor(memDC, m_textColor); SelectObject(memDC, m_font); PaintTimeline(memDC, timelineRect); BitBlt(hdc, 0, rect.top, rect.right - rect.left, rect.bottom - rect.top, memDC, 0, rect.top, SRCCOPY); }); } */ } TRACKMOUSEEVENT tme; tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; tme.hwndTrack = hWnd; TrackMouseEvent(&tme); m_mouseOverWindow = true; break; } case WM_MOUSELEAVE: m_mouseOverWindow = false; TRACKMOUSEEVENT tme; tme.cbSize = sizeof(tme); tme.dwFlags = TME_CANCEL; tme.hwndTrack = hWnd; TrackMouseEvent(&tme); if (!m_showPopup) UnselectAndRedraw(); break; case WM_MBUTTONDOWN: { POINTS p = MAKEPOINTS(lParam); StartDragToScroll(POINT{ p.x, p.y }); break; } case WM_LBUTTONDOWN: { if (!m_parentHwnd && m_traceView.sessions.empty()) { RECT r; GetClientRect(hWnd, &r); int centerHrz = r.right/2; int centerVrt = r.bottom/2; r = { centerHrz, centerVrt, centerHrz, centerVrt }; InflateRect(&r, 180, 40); POINTS p = MAKEPOINTS(lParam); if (PtInRect(&r, { p.x, p.y })) { OPENFILENAME ofn; tchar szFile[MAX_PATH] = TC(""); ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = m_hwnd; ofn.lpstrFilter = L"Uba Files\0*.uba\0All Files\0*.*\0"; ofn.lpstrFile = szFile; ofn.nMaxFile = MAX_PATH; ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; if (GetOpenFileName(&ofn)) { m_fileName.Append(szFile); PostNewTrace(0, false); } } break; } if (m_buttonSelected != ~0u) { bool* values = &m_config.showProgress; values[m_buttonSelected] = !values[m_buttonSelected]; HitTestResult res; HitTest(res, { -1, -1 }); UpdateScrollbars(true); Redraw(false); } else if (m_timelineSelected != 0) { if (!m_client) // Does not work for network streams { float timelineSelected = Max(m_timelineSelected, 0.0f); Reset(); bool changed; u64 time = MsToTime(u64(timelineSelected * 1000.0)); if (!m_fileName.IsEmpty()) { if (!m_trace.ReadFile(m_traceView, m_fileName.data, true)) return false; } else { if (!m_trace.StartReadNamed(m_traceView, nullptr, true, true)) return false; if (m_traceView.realStartTime + time > m_startTime) time = m_startTime - m_traceView.realStartTime; } m_traceView.finished = false; if (!m_fileName.IsEmpty()) m_trace.UpdateReadFile(m_traceView, time, changed); else if (m_usingNamed) m_trace.UpdateReadNamed(m_traceView, time, changed); m_pauseStart = m_startTime + time; m_pauseTime = m_startTime - m_pauseStart; if (!m_paused) { m_autoScroll = true; m_replay = 1; SetTimer(m_hwnd, 0, 200, NULL); } else { m_pauseTime = 0; } HitTestResult res; HitTest(res, { -1, -1 }); RECT r; GetClientRect(hWnd, &r); m_scrollPosX = Min(Max(m_scrollPosX, float(r.right - m_contentWidth)), 0.0f); m_scrollPosY = Min(0.0f, Max(m_scrollPosY, float(r.bottom - m_contentHeight))); UpdateScrollbars(true); Redraw(true); } } else if (!m_hyperLinkSelected.empty()) { ShellExecuteW(NULL, L"open", m_hyperLinkSelected.c_str(), NULL, NULL, SW_SHOW); } else if (m_sessionSelectedIndex != ~0u && !m_traceView.sessions[m_sessionSelectedIndex].hyperlink.empty()) { ShellExecuteW(NULL, L"open", m_traceView.sessions[m_sessionSelectedIndex].hyperlink.c_str(), NULL, NULL, SW_SHOW); } else { POINTS p = MAKEPOINTS(lParam); StartDragToScroll(POINT{ p.x, p.y }); } break; } case WM_SETCURSOR: { static HCURSOR arrow = LoadCursorW(NULL, IDC_ARROW); static HCURSOR hand = LoadCursorW(NULL, IDC_HAND); bool useHand = false; if (!m_parentHwnd && m_traceView.sessions.empty()) { RECT r; GetClientRect(hWnd, &r); int centerHrz = r.right/2; int centerVrt = r.bottom/2; r = { centerHrz, centerVrt, centerHrz, centerVrt }; InflateRect(&r, 180, 40); POINT pt; GetCursorPos(&pt); ScreenToClient(hWnd, &pt); useHand = PtInRect(&r, { pt.x, pt.y }); } else useHand = !m_hyperLinkSelected.empty() || (m_sessionSelectedIndex != ~0u && !m_traceView.sessions[m_sessionSelectedIndex].hyperlink.empty()); SetCursor(useHand ? hand : arrow); break; } case WM_LBUTTONUP: { if (!(m_buttonSelected != ~0u || m_timelineSelected != 0)) { StopDragToScroll(); } break; } case WM_RBUTTONUP: { POINT point; point.x = LOWORD(lParam); point.y = HIWORD(lParam); HMENU hMenu = CreatePopupMenu(); ClientToScreen(hWnd, &point); #define UBA_VISUALIZER_FLAG(name, defaultValue, desc) \ AppendMenuW(hMenu, MF_STRING | (m_config.name ? MF_CHECKED : 0), Popup_##name, desc); UBA_VISUALIZER_FLAGS2 #undef UBA_VISUALIZER_FLAG AppendMenuW(hMenu, MF_STRING, Popup_IncreaseFontSize, L"&Increase Font Size"); AppendMenuW(hMenu, MF_STRING, Popup_DecreaseFontSize, L"&Decrease Font Size"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); if (m_sessionSelectedIndex != ~0u) { AppendMenuW(hMenu, MF_STRING, Popup_CopySessionInfo, L"&Copy Session Info"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); } else if (m_processSelected) { const TraceView::Process& process = m_traceView.GetProcess(m_processSelectedLocation); AppendMenuW(hMenu, MF_STRING, Popup_CopyProcessInfo, L"&Copy Process Info"); if (!process.logLines.empty()) AppendMenuW(hMenu, MF_STRING, Popup_CopyProcessLog, L"Copy Process &Log"); if (!process.breadcrumbs.empty()) AppendMenuW(hMenu, MF_STRING, Popup_CopyProcessBreadcrumbs, L"Copy Process &Breadcrumbs"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); } else if (m_workSelected) { AppendMenuW(hMenu, MF_STRING, Popup_CopyWorkInfo, L"&Copy Work Info"); } if (!m_traceView.sessions.empty()) { if (!m_client) { if (!m_replay || m_traceView.finished) AppendMenuW(hMenu, MF_STRING, Popup_Replay, L"&Replay Trace"); else { if (m_paused) AppendMenuW(hMenu, MF_STRING, Popup_Play, L"&Play"); else AppendMenuW(hMenu, MF_STRING, Popup_Pause, L"&Pause"); AppendMenuW(hMenu, MF_STRING, Popup_JumpToEnd, L"&Jump To End"); } } if (m_fileName.IsEmpty()) AppendMenuW(hMenu, MF_STRING, Popup_SaveAs, L"&Save Trace"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); } AppendMenuW(hMenu, MF_STRING, Popup_SaveSettings, L"Save Position/Settings"); AppendMenuW(hMenu, MF_STRING, Popup_OpenSettings, L"Open Settings file"); AppendMenuW(hMenu, MF_STRING, Popup_Quit, L"&Quit"); m_showPopup = true; switch (TrackPopupMenu(hMenu, TPM_RETURNCMD | TPM_RIGHTBUTTON, point.x, point.y, 0, hWnd, NULL)) { case Popup_SaveAs: { OPENFILENAME ofn; TCHAR szFile[260] = { 0 }; // Initialize OPENFILENAME ZeroMemory(&ofn, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = hWnd; ofn.lpstrFile = szFile; ofn.nMaxFile = sizeof(szFile); ofn.lpstrDefExt = TC("uba"); ofn.lpstrFilter = TC("Uba\0*.uba\0All\0*.*\0"); ofn.nFilterIndex = 1; ofn.lpstrFileTitle = NULL; ofn.nMaxFileTitle = 0; ofn.lpstrInitialDir = NULL; //ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; if (GetSaveFileName(&ofn)) m_trace.SaveAs(ofn.lpstrFile); break; } case Popup_ShowProcessText: m_config.ShowProcessText = !m_config.ShowProcessText; Redraw(true); break; case Popup_ShowReadWriteColors: m_config.ShowReadWriteColors = !m_config.ShowReadWriteColors; DirtyBitmaps(false); Redraw(true); break; case Popup_ScaleHorizontalWithScrollWheel: m_config.ScaleHorizontalWithScrollWheel = !m_config.ScaleHorizontalWithScrollWheel; break; case Popup_ShowAllTraces: m_config.ShowAllTraces = !m_config.ShowAllTraces; break; case Popup_SortActiveRemoteSessions: m_config.SortActiveRemoteSessions = !m_config.SortActiveRemoteSessions; Redraw(true); break; case Popup_AutoScaleHorizontal: m_config.AutoScaleHorizontal = !m_config.AutoScaleHorizontal; UpdateScrollbars(true); Redraw(true); break; case Popup_LockTimelineToBottom: m_config.LockTimelineToBottom = !m_config.LockTimelineToBottom; Redraw(true); break; case Popup_DarkMode: { m_config.DarkMode = !m_config.DarkMode; DirtyBitmaps(false); InitBrushes(); UpdateTheme(); Redraw(true); break; } case Popup_AutoSaveSettings: m_config.AutoSaveSettings = !m_config.AutoSaveSettings; break; case Popup_Replay: PostNewTrace(1, false); break; case Popup_Play: Pause(false); break; case Popup_Pause: Pause(true); break; case Popup_JumpToEnd: m_traceView.finished = true; PostNewTrace(0, false); break; case Popup_SaveSettings: SaveSettings(); break; case Popup_OpenSettings: ShellExecuteW(NULL, L"open", m_config.filename.c_str(), NULL, NULL, SW_SHOW); break; case Popup_Quit: // Quit PostQuit(); break; case Popup_IncreaseFontSize: ChangeFontSize(1); break; case Popup_DecreaseFontSize: ChangeFontSize(-1); break; case Popup_CopySessionInfo: { TString str; auto& session = m_traceView.sessions[m_sessionSelectedIndex]; str.append(session.fullName).append(TC("\n")); for (auto& line : session.summary) str.append(line).append(TC("\n")); CopyTextToClipboard(str); break; } case Popup_CopyProcessInfo: { TString str; WriteTextLogger logger(str); const TraceView::Process& process = m_traceView.GetProcess(m_processSelectedLocation); WriteProcessStats(logger, process); CopyTextToClipboard(str); break; } case Popup_CopyProcessLog: { TString str; const TraceView::Process& process = m_traceView.GetProcess(m_processSelectedLocation); bool isFirst = true; for (auto line : process.logLines) { if (!isFirst) str += '\n'; isFirst = false; str += line.text; } CopyTextToClipboard(str); break; } case Popup_CopyProcessBreadcrumbs: { const TraceView::Process& process = m_traceView.GetProcess(m_processSelectedLocation); CopyTextToClipboard(process.breadcrumbs); break; } case Popup_CopyWorkInfo: { TString str; WriteTextLogger logger(str); auto& record = m_traceView.workTracks[m_workTrack].records[m_workIndex]; WriteWorkStats(logger, record); CopyTextToClipboard(str); break; } } DestroyMenu(hMenu); m_showPopup = false; UnselectAndRedraw(); break; } case WM_MBUTTONUP: { StopDragToScroll(); //m_processSelected = false; break; } case WM_KEYDOWN: { if (wParam == VK_SPACE) Pause(!m_paused); if (wParam == VK_ADD) ++m_replay; if (wParam == VK_SUBTRACT) --m_replay; if (wParam == VK_BACK) if (!m_filterString.empty()) { m_filterString.resize(m_filterString.size() -1); Redraw(true); } break; } case WM_CHAR: { tchar c = static_cast(wParam); if (c > 32 && c != '\t' && c != '\n') m_filterString += static_cast(wParam); Redraw(true); } case WM_VSCROLL: { RECT r; GetClientRect(hWnd, &r); float oldScrollY = m_scrollPosY; // HIWORD(wParam) only carries 16-bits, so use GetScrollInfo for larger scroll areas SCROLLINFO scrollInfo = {}; scrollInfo.cbSize = sizeof(scrollInfo); scrollInfo.fMask = SIF_TRACKPOS; GetScrollInfo(m_hwnd, SB_VERT, &scrollInfo); switch (LOWORD(wParam)) { case SB_THUMBTRACK: case SB_THUMBPOSITION: m_scrollPosY = -float(scrollInfo.nTrackPos); break; case SB_PAGEDOWN: m_scrollPosY = m_scrollPosY - float(r.bottom); break; case SB_PAGEUP: m_scrollPosY = m_scrollPosY + float(r.bottom); break; case SB_LINEDOWN: m_scrollPosY -= 30; break; case SB_LINEUP: m_scrollPosY += 30; break; } m_scrollPosY = Min(Max(m_scrollPosY, float(r.bottom - m_contentHeight)), 0.0f); if (oldScrollY != m_scrollPosY) { UpdateScrollbars(true); Redraw(false); } return 0; } case WM_HSCROLL: { RECT r; GetClientRect(hWnd, &r); float oldScrollX = m_scrollPosX; bool autoScroll = false; // HIWORD(wParam) only carries 16-bits, so use GetScrollInfo for larger scroll areas SCROLLINFO scrollInfo = {}; scrollInfo.cbSize = sizeof(scrollInfo); scrollInfo.fMask = SIF_TRACKPOS; GetScrollInfo(m_hwnd, SB_HORZ, &scrollInfo); switch (LOWORD(wParam)) { case SB_THUMBTRACK: m_scrollPosX = -float(scrollInfo.nTrackPos); if (m_contentWidthWhenThumbTrack == 0) m_contentWidthWhenThumbTrack = m_contentWidth; break; case SB_THUMBPOSITION: autoScroll = m_contentWidthWhenThumbTrack - r.right <= HIWORD(wParam) + 10; m_contentWidthWhenThumbTrack = 0; m_scrollPosX = -float(scrollInfo.nTrackPos); break; case SB_PAGEDOWN: m_scrollPosX = m_scrollPosX - float(r.right); break; case SB_PAGEUP: m_scrollPosX = m_scrollPosX + float(r.right); break; case SB_LINEDOWN: m_scrollPosX -= 30; break; case SB_LINEUP: m_scrollPosX += 30; break; case SB_ENDSCROLL: return 0; } int minScroll = r.right - m_contentWidth; m_autoScroll = !m_traceView.finished && (m_scrollPosX <= float(minScroll) || autoScroll); m_scrollPosX = Min(Max(m_scrollPosX, float(r.right - m_contentWidth)), 0.0f); if (oldScrollX != m_scrollPosX) { UpdateScrollbars(true); Redraw(false); } return 0; } } return DefWindowProc(hWnd, Msg, wParam, lParam); } LRESULT CALLBACK Visualizer::StaticWinProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { auto thisPtr = (Visualizer*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (!thisPtr && Msg == WM_CREATE) { thisPtr = (Visualizer*)lParam; SetWindowLongPtr(hWnd, GWLP_USERDATA, lParam); //EnableNonClientDpiScaling(hWnd); } if (thisPtr && hWnd == thisPtr->m_hwnd) return thisPtr->WinProc(hWnd, Msg, wParam, lParam); else return DefWindowProc(hWnd, Msg, wParam, lParam); } }