Showing preview only (571K chars total). Download the full file or copy to clipboard to get everything.
Repository: haskell/ThreadScope
Branch: master
Commit: 5682975d3816
Files: 81
Total size: 543.7 KB
Directory structure:
gitextract_n4vjqfzb/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Events/
│ ├── EventDuration.hs
│ ├── EventTree.hs
│ ├── HECs.hs
│ ├── ReadEvents.hs
│ ├── SparkStats.hs
│ ├── SparkTree.hs
│ └── TestEvents.hs
├── GUI/
│ ├── App.hs
│ ├── BookmarkView.hs
│ ├── ConcurrencyControl.hs
│ ├── DataFiles.hs
│ ├── Dialogs.hs
│ ├── EventsView.hs
│ ├── GtkExtras.hs
│ ├── Histogram.hs
│ ├── KeyView.hs
│ ├── Main.hs
│ ├── MainWindow.hs
│ ├── ProgressView.hs
│ ├── SaveAs.hs
│ ├── StartupInfoView.hs
│ ├── SummaryView.hs
│ ├── Timeline/
│ │ ├── Activity.hs
│ │ ├── CairoDrawing.hs
│ │ ├── HEC.hs
│ │ ├── Motion.hs
│ │ ├── Render/
│ │ │ └── Constants.hs
│ │ ├── Render.hs
│ │ ├── Sparks.hs
│ │ ├── Ticks.hs
│ │ └── Types.hs
│ ├── Timeline.hs
│ ├── TraceView.hs
│ ├── Types.hs
│ └── ViewerColours.hs
├── Graphics/
│ └── UI/
│ └── Gtk/
│ └── ModelView/
│ └── TreeView/
│ └── Compat.hs
├── LICENSE
├── Main.hs
├── Makefile
├── README.md
├── Setup.hs
├── TODO
├── cabal.project
├── cabal.project.osx
├── include/
│ └── windows_cconv.h
├── index.html
├── papers/
│ └── haskell_symposium_2009/
│ ├── Makefile
│ ├── bsort/
│ │ ├── BSort.hs
│ │ ├── BSortPar.hs
│ │ ├── BSortPar2.hs
│ │ ├── BSortStreaming.hs
│ │ └── Makefile
│ ├── bsort.tex
│ ├── fib/
│ │ ├── Fib1.hs
│ │ ├── Fib2.hs
│ │ └── Makefile
│ ├── ghc-parallel-tuning.bib
│ ├── ghc-parallel-tuning.tex
│ ├── infrastructure.tex
│ ├── motivation.tex
│ ├── related-work.tex
│ ├── sigplanconf.cls
│ ├── sumEuler/
│ │ ├── Makefile
│ │ ├── SumEuler0.hs
│ │ ├── SumEuler1.hs
│ │ ├── SumEuler2.hs
│ │ └── SumEuler3.hs
│ └── threadring.tex
├── scripts/
│ └── install-on-osx.sh
├── stack.osx.yaml
├── stack.yaml
├── tests/
│ ├── Hello.hs
│ ├── Makefile
│ ├── Null.hs
│ ├── ParFib.hs
│ └── SumEulerPar1.hs
├── threadscope.cabal
└── threadscope.ui
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- master
tags:
- v*
pull_request:
release:
env:
GHC_FOR_RELEASE: "9.10"
jobs:
build:
name: GHC ${{ matrix.ghc-version }} on ${{ matrix.os }} ${{matrix.container}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
ghc-version: ['9.12', '9.10', '9.8', '9.6', '9.4', '9.2']
container: ['']
include:
# The windows build is currently broken
# See #135
- os: windows-latest
ghc-version: '9.10'
- os: macos-latest
ghc-version: '9.10'
# gtk2hs is broken under apline
# See https://github.com/gtk2hs/gtk2hs/issues/262
#- os: ubuntu-latest
# ghc-version: '9.10'
# container: alpine:3.21
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@v4
- name: Install system dependencies (Alpine)
if: ${{ startsWith(matrix.container, 'alpine') }}
shell: sh
run: |
apk add bash curl sudo jq pkgconfig \
zlib-dev zlib-static binutils curl \
gcc g++ gmp-dev libc-dev libffi-dev make \
musl-dev ncurses-dev perl tar xz \
gtk+3.0-dev
- name: Install system dependencies (Ubuntu)
if: runner.os == 'Linux' && !startsWith(matrix.container, 'alpine')
run: sudo apt-get update && sudo apt-get install libgtk-3-dev
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: brew install cairo gtk+3 pkg-config
- name: Set extra cabal build options (macOS)
if: runner.os == 'macOS'
run: |
printf 'package gtk\n flags: +have-quartz-gtk' >>cabal.project
- name: Set up GHC ${{ matrix.ghc-version }}
uses: haskell-actions/setup@v2
id: setup
with:
ghc-version: ${{ matrix.ghc-version }}
# Taken from https://github.com/agda/agda/blob/8210048a50c35d8d6fd0ae7e5edd1699592fda6f/src/github/workflows/cabal.yml#L113C1-L124C85
# See: https://github.com/haskell/text-icu/pull/86
# pacman needs MSYS /usr/bin in PATH, but this breaks the latest cache action.
# - https://github.com/actions/cache/issues/1073
# MSYS' pkg-config needs MSYS /mingw64/bin which we can safely add to the PATH
#
- name: Install system dependencies (Windows)
if: ${{ startsWith(matrix.os, 'windows') }}
shell: pwsh
run: |
$env:PATH = "C:\msys64\usr\bin;$env:PATH"
pacman --noconfirm -S msys2-keyring mingw-w64-x86_64-pkgconf mingw-w64-x86_64-gtk3
echo "C:\msys64\mingw64\bin" | Out-File -FilePath "$env:GITHUB_PATH" -Append
- name: Enable static build (only on alpine)
if: ${{ startsWith(matrix.container, 'alpine') }}
run: |
echo 'executable-static: true' >>cabal.project
echo 'cc-options: -D_Noreturn=' >>cabal.project
- name: Configure the build
run: |
cabal configure --enable-tests --enable-benchmarks --disable-documentation
cabal build all --dry-run
- name: Restore cached dependencies
uses: actions/cache/restore@v4
id: cache
env:
key: ${{ runner.os }}${{ matrix.container && '-container-' }}${{matrix.container}}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
with:
path: ${{ steps.setup.outputs.cabal-store }}
key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
restore-keys: ${{ env.key }}-
- name: Install dependencies
# If we had an exact cache hit, the dependencies will be up to date.
if: steps.cache.outputs.cache-hit != 'true'
run: cabal build all --only-dependencies
# Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
- name: Save cached dependencies
uses: actions/cache/save@v4
# If we had an exact cache hit, trying to save the cache would error because of key clash.
if: steps.cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.setup.outputs.cabal-store }}
key: ${{ steps.cache.outputs.cache-primary-key }}
- name: Build
run: cabal build all
- name: Run tests
run: cabal test all
- name: Check cabal file
run: cabal check
- name: Create bindist
shell: sh
run: |
cabal install --install-method=copy --installdir=dist
BINDIST_NAME="threadscope-ghc-${{matrix.ghc-version}}-${{ matrix.os }}${{ matrix.container && '-' }}${{matrix.container && 'alpine'}}"
echo "BINDIST_NAME=$BINDIST_NAME" >> "$GITHUB_ENV"
tar -cJf "$BINDIST_NAME.tar.xz" -C dist threadscope
echo bindist is "$BINDIST_NAME.tar.xz"
- name: Upload bindist to artifacts
uses: actions/upload-artifact@v4
if: ${{ matrix.ghc-version == env.GHC_FOR_RELEASE }}
with:
name: ${{ env.BINDIST_NAME }}
path: ${{ env.BINDIST_NAME}}.tar.xz
- name: Release
uses: softprops/action-gh-release@v2
if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.ghc-version == env.GHC_FOR_RELEASE }}
with:
files: ${{ env.BINDIST_NAME }}.tar.xz
================================================
FILE: .gitignore
================================================
dist-newstyle
cabal.project.local~*
================================================
FILE: CHANGELOG.md
================================================
# Revision history for threadscope
## 2025-05-29 - v0.2.15.0
* Switch to GTK3 ([#137](https://github.com/haskell/ThreadScope/pull/137)))
* Support new versions of GHC up to 9.12 and dependencies.
## 2022-05-10 - v0.2.14.1
* Spelling fixes ([#121](https://github.com/haskell/ThreadScope/pull/121), [#123](https://github.com/haskell/ThreadScope/pull/123))
* Add compatibility with GHC-9.2 ([#124](https://github.com/haskell/ThreadScope/pull/124), [#125](https://github.com/haskell/ThreadScope/pull/125))
* Update dependencies ([#126](https://github.com/haskell/ThreadScope/pull/126))
## 2021-01-09 - v0.2.14
* Print times with more sensible units ([#111](https://github.com/haskell/ThreadScope/pull/111))
* EventDuration: Make it more robust to truncated eventlogs ([#110](https://github.com/haskell/ThreadScope/pull/110))
* Use GitHub Actions for CI ([#113](https://github.com/haskell/ThreadScope/pull/113))
* Relax upper version bounds for ghc-events, time, bytestring, and template-haskell
## 2020-04-06 - v0.2.13
* Add changelog to extra-source-files ([#105](https://github.com/haskell/ThreadScope/pull/105))
* Fix broken GitHub Releases deployment ([#106](https://github.com/haskell/ThreadScope/pull/106))
* Update ghc-events to 0.13.0 ([#107](https://github.com/haskell/ThreadScope/pull/107))
* Relax upper version bound for time
## 2020-03-04 - v0.2.12
* Remove unused events entry box ([#93](https://github.com/haskell/ThreadScope/pull/93))
* Make the app work even if it fails to load the logo ([#96](https://github.com/haskell/ThreadScope/pull/96))
* Support GHC 8.8 ([#99](https://github.com/haskell/ThreadScope/pull/99))
* Support ghc-events 0.12.0 ([#101](https://github.com/haskell/ThreadScope/pull/101))
* Stop using gtk-mac-integration and fix broken CI ([#103](https://github.com/haskell/ThreadScope/pull/103))
* This causes a visual regression. The logo won't be displayed in Dock.
## 2018-07-12 - v0.2.11.1
* Relax upper version bounds for containers and ghc-events (#88)
## 2018-06-08 - v0.2.11
* Relax upper version bounds for template-haskell and temporary
* Fix build failure with gtk-0.14.9
* Modernise AppVeyor CI script
## 2018-02-16 - v0.2.10
* Add instructions to install gtk2 in the README
* Do not include windows_cconv.h on non mingw32 systems (#79)
* Relax upper version bound for ghc-events (#80)
* Relax upper version bound for time
## 2017-09-02 - v0.2.9
* Render GC waiting periods in light orange (#70)
* Fix inappropriate calling convention on Windows x86 (#71)
* Enable GitHub Releases (#75)
## 2017-07-17 - v0.2.8
* Add macOS support (#56)
* Update ghc-events to 0.6.0 (#61)
* CI builds for Linux/Windows/macOS (#64, #65)
* Set upper version bounds for dependencies
================================================
FILE: Events/EventDuration.hs
================================================
-- This module supports a duration-based data-type to represent thread
-- execution and GC information.
module Events.EventDuration (
EventDuration(..),
isGCDuration,
startTimeOf, endTimeOf, durationOf,
eventsToDurations,
isDiscreteEvent
) where
import System.IO
import System.IO.Unsafe
-- Imports for GHC Events
import GHC.RTS.Events hiding (Event, GCIdle, GCWork)
import qualified GHC.RTS.Events as GHC
-------------------------------------------------------------------------------
-- This data structure is a duration-based representation of the eventlog
-- information where thread-runs and GCs are explicitly represented by a
-- single constructor identifying their start and end points.
data EventDuration
= ThreadRun {-#UNPACK#-}!ThreadId
ThreadStopStatus
{-#UNPACK#-}!Timestamp
{-#UNPACK#-}!Timestamp
| GCStart {-#UNPACK#-}!Timestamp
{-#UNPACK#-}!Timestamp
| GCWork {-#UNPACK#-}!Timestamp
{-#UNPACK#-}!Timestamp
| GCIdle {-#UNPACK#-}!Timestamp
{-#UNPACK#-}!Timestamp
| GCEnd {-#UNPACK#-}!Timestamp
{-#UNPACK#-}!Timestamp
deriving Show
{-
GCStart GCWork GCIdle GCEnd
gc start -----> work -----> idle ------+> done -----> gc end
| |
`-------<-------<-----'
-}
isGCDuration :: EventDuration -> Bool
isGCDuration GCStart{} = True
isGCDuration GCWork{} = True
isGCDuration GCIdle{} = True
isGCDuration GCEnd{} = True
isGCDuration _ = False
-------------------------------------------------------------------------------
-- The start time of an event.
startTimeOf :: EventDuration -> Timestamp
startTimeOf ed
= case ed of
ThreadRun _ _ startTime _ -> startTime
GCStart startTime _ -> startTime
GCWork startTime _ -> startTime
GCIdle startTime _ -> startTime
GCEnd startTime _ -> startTime
-------------------------------------------------------------------------------
-- The emd time of an event.
endTimeOf :: EventDuration -> Timestamp
endTimeOf ed
= case ed of
ThreadRun _ _ _ endTime -> endTime
GCStart _ endTime -> endTime
GCWork _ endTime -> endTime
GCIdle _ endTime -> endTime
GCEnd _ endTime -> endTime
-------------------------------------------------------------------------------
-- The duration of an EventDuration
durationOf :: EventDuration -> Timestamp
durationOf ed = endTimeOf ed - startTimeOf ed
-------------------------------------------------------------------------------
eventsToDurations :: [GHC.Event] -> [EventDuration]
eventsToDurations [] = []
eventsToDurations (event : events) =
case evSpec event of
RunThread{thread=t}
| Just ev <- runDuration t -> ev : rest
| otherwise -> rest
StopThread{} -> rest
StartGC -> gcStart (evTime event) events
EndGC{} -> rest
_otherEvent -> rest
where
rest = eventsToDurations events
runDuration :: ThreadId -> Maybe EventDuration
runDuration t = do
(endTime, s) <- findRunThreadTime events
return $ ThreadRun t s (evTime event) endTime
isDiscreteEvent :: GHC.Event -> Bool
isDiscreteEvent e =
case evSpec e of
RunThread{} -> False
StopThread{} -> False
StartGC{} -> False
EndGC{} -> False
GHC.GCWork{} -> False
GHC.GCIdle{} -> False
GHC.GCDone{} -> False
GHC.SparkCounters{} -> False
_ -> True
gcStart :: Timestamp -> [GHC.Event] -> [EventDuration]
gcStart _ [] = []
gcStart t0 (event : events) =
case evSpec event of
GHC.GCWork{} -> GCStart t0 t1 : gcWork t1 events
GHC.GCIdle{} -> GCStart t0 t1 : gcIdle t1 events
GHC.GCDone{} -> GCStart t0 t1 : gcDone t1 events
GHC.EndGC{} -> GCStart t0 t1 : eventsToDurations events
RunThread{} -> GCStart t0 t1 : eventsToDurations (event : events)
_other -> gcStart t0 events
where
t1 = evTime event
gcWork :: Timestamp -> [GHC.Event] -> [EventDuration]
gcWork _ [] = []
gcWork t0 (event : events) =
case evSpec event of
GHC.GCWork{} -> gcWork t0 events
GHC.GCIdle{} -> GCWork t0 t1 : gcIdle t1 events
GHC.GCDone{} -> GCWork t0 t1 : gcDone t1 events
GHC.EndGC{} -> GCWork t0 t1 : eventsToDurations events
RunThread{} -> GCWork t0 t1 : eventsToDurations (event : events)
_other -> gcStart t0 events
where
t1 = evTime event
gcIdle :: Timestamp -> [GHC.Event] -> [EventDuration]
gcIdle _ [] = []
gcIdle t0 (event : events) =
case evSpec event of
GHC.GCIdle{} -> gcIdle t0 events
GHC.GCWork{} -> GCIdle t0 t1 : gcWork t1 events
GHC.GCDone{} -> GCIdle t0 t1 : gcDone t1 events
GHC.EndGC{} -> GCIdle t0 t1 : eventsToDurations events
RunThread{} -> GCIdle t0 t1 : eventsToDurations (event : events)
_other -> gcStart t0 events
where
t1 = evTime event
gcDone :: Timestamp -> [GHC.Event] -> [EventDuration]
gcDone _ [] = []
gcDone t0 (event : events) =
case evSpec event of
GHC.GCDone{} -> gcDone t0 events
GHC.GCWork{} -> GCEnd t0 t1 : gcWork t1 events
GHC.GCIdle{} -> GCEnd t0 t1 : gcIdle t1 events
GHC.EndGC{} -> GCEnd t0 t1 : eventsToDurations events
RunThread{} -> GCEnd t0 t1 : eventsToDurations (event : events)
_other -> gcStart t0 events
where
t1 = evTime event
-------------------------------------------------------------------------------
findRunThreadTime :: [GHC.Event] -> Maybe (Timestamp, ThreadStopStatus)
findRunThreadTime [] = Nothing
findRunThreadTime (e : es)
= case evSpec e of
StopThread{status=s} -> Just (evTime e, s)
_ | [] <- es -> unsafePerformIO $ do
hPutStrLn stderr "warning: failed to find stop event for thread; eventlog truncated?"
return $ Just (evTime e, NoStatus)
-- the eventlog abruptly ended; presumably the
-- thread was still running.
| otherwise -> findRunThreadTime es
-------------------------------------------------------------------------------
================================================
FILE: Events/EventTree.hs
================================================
module Events.EventTree (
DurationTree(..),
mkDurationTree,
runTimeOf, gcTimeOf,
reportDurationTree,
durationTreeCountNodes,
durationTreeMaxDepth,
EventTree(..), EventNode(..),
mkEventTree,
reportEventTree, eventTreeMaxDepth,
) where
import Events.EventDuration
import GHC.RTS.Events hiding (Event)
import qualified GHC.RTS.Events as GHC
import Control.Exception (assert)
import Text.Printf
-------------------------------------------------------------------------------
-- We map the events onto a binary search tree, so that we can easily
-- find the events that correspond to a particular view of the
-- timeline. Additionally, each node of the tree contains a summary
-- of the information below it, so that we can render views at various
-- levels of resolution. For example, if a tree node would represent
-- less than one pixel on the display, there is no point is descending
-- the tree further.
-- We only split at event boundaries; we never split an event into
-- multiple pieces. Therefore, the binary tree is only roughly split
-- by time, the actual split depends on the distribution of events
-- below it.
data DurationTree
= DurationSplit
{-#UNPACK#-}!Timestamp -- The start time of this run-span
{-#UNPACK#-}!Timestamp -- The time used to split the events into two parts
{-#UNPACK#-}!Timestamp -- The end time of this run-span
DurationTree -- The LHS split; all events lie completely between
-- start and split
DurationTree -- The RHS split; all events lie completely between
-- split and end
{-#UNPACK#-}!Timestamp -- The total amount of time spent running a thread
{-#UNPACK#-}!Timestamp -- The total amount of time spend in GC
| DurationTreeLeaf
EventDuration
| DurationTreeEmpty
deriving Show
-------------------------------------------------------------------------------
mkDurationTree :: [EventDuration] -> Timestamp -> DurationTree
mkDurationTree es endTime =
-- trace (show tree) $
tree
where
tree = splitDurations es endTime
splitDurations :: [EventDuration] -- events
-> Timestamp -- end time of last event in the list
-> DurationTree
splitDurations [] _endTime =
-- if len /= 0 then error "splitDurations0" else
DurationTreeEmpty -- The case for an empty list of events.
splitDurations [e] _entTime =
DurationTreeLeaf e
splitDurations es endTime
| null rhs
= splitDurations es lhs_end
| null lhs
= error $
printf "splitDurations: null lhs: len = %d, startTime = %d, endTime = %d\n"
(length es) startTime endTime
++ '\n': show es
| otherwise
= -- trace (printf "len = %d, startTime = %d, endTime = %d, lhs_len = %d\n" len startTime endTime lhs_len) $
assert (length lhs + length rhs == length es) $
DurationSplit startTime
lhs_end
endTime
ltree
rtree
runTime
gcTime
where
startTime = startTimeOf (head es)
splitTime = startTime + (endTime - startTime) `div` 2
(lhs, lhs_end, rhs) = splitDurationList es [] splitTime 0
ltree = splitDurations lhs lhs_end
rtree = splitDurations rhs endTime
runTime = runTimeOf ltree + runTimeOf rtree
gcTime = gcTimeOf ltree + gcTimeOf rtree
splitDurationList :: [EventDuration]
-> [EventDuration]
-> Timestamp
-> Timestamp
-> ([EventDuration], Timestamp, [EventDuration])
splitDurationList [] acc !_tsplit !tmax
= (reverse acc, tmax, [])
splitDurationList [e] acc !_tsplit !tmax
-- Just one event left: put it on the right. This ensures that we
-- have at least one event on each side of the split.
= (reverse acc, tmax, [e])
splitDurationList (e:es) acc !tsplit !tmax
| tstart <= tsplit -- pick all events that start at or before the split
= splitDurationList es (e:acc) tsplit (max tmax tend)
| otherwise
= (reverse acc, tmax, e:es)
where
tstart = startTimeOf e
tend = endTimeOf e
-------------------------------------------------------------------------------
runTimeOf :: DurationTree -> Timestamp
runTimeOf (DurationSplit _ _ _ _ _ runTime _) = runTime
runTimeOf (DurationTreeLeaf e) | ThreadRun{} <- e = durationOf e
runTimeOf _ = 0
-------------------------------------------------------------------------------
gcTimeOf :: DurationTree -> Timestamp
gcTimeOf (DurationSplit _ _ _ _ _ _ gcTime) = gcTime
gcTimeOf (DurationTreeLeaf e) | isGCDuration e = durationOf e
gcTimeOf _ = 0
-------------------------------------------------------------------------------
reportDurationTree :: Int -> DurationTree -> IO ()
reportDurationTree hecNumber eventTree
= putStrLn ("HEC " ++ show hecNumber ++ reportText)
where
reportText = " nodes = " ++ show (durationTreeCountNodes eventTree) ++
" max depth = " ++ show (durationTreeMaxDepth eventTree)
-------------------------------------------------------------------------------
durationTreeCountNodes :: DurationTree -> Int
durationTreeCountNodes (DurationSplit _ _ _ lhs rhs _ _)
= 1 + durationTreeCountNodes lhs + durationTreeCountNodes rhs
durationTreeCountNodes _ = 1
-------------------------------------------------------------------------------
durationTreeMaxDepth :: DurationTree -> Int
durationTreeMaxDepth (DurationSplit _ _ _ lhs rhs _ _)
= 1 + durationTreeMaxDepth lhs `max` durationTreeMaxDepth rhs
durationTreeMaxDepth _ = 1
-------------------------------------------------------------------------------
data EventTree
= EventTree
{-#UNPACK#-}!Timestamp -- The start time of this run-span
{-#UNPACK#-}!Timestamp -- The end time of this run-span
EventNode
data EventNode
= EventSplit
{-#UNPACK#-}!Timestamp -- The time used to split the events into two parts
EventNode -- The LHS split; all events lie completely between
-- start and split
EventNode -- The RHS split; all events lie completely between
-- split and end
| EventTreeLeaf [GHC.Event]
-- sometimes events happen "simultaneously" (at the same time
-- given the resolution of our clock source), so we can't
-- separate them.
| EventTreeOne GHC.Event
-- This is a space optimisation for the common case of
-- EventTreeLeaf [e].
mkEventTree :: [GHC.Event] -> Timestamp -> EventTree
mkEventTree es endTime =
EventTree s e $
-- trace (show tree) $
tree
where
tree = splitEvents es endTime
(s,e) = if null es then (0,0) else (evTime (head es), endTime)
splitEvents :: [GHC.Event] -- events
-> Timestamp -- end time of last event in the list
-> EventNode
splitEvents [] !_endTime =
-- if len /= 0 then error "splitEvents0" else
EventTreeLeaf [] -- The case for an empty list of events
splitEvents [e] !_endTime =
EventTreeOne e
splitEvents es !endTime
| duration == 0
= EventTreeLeaf es
| null rhs
= splitEvents es lhs_end
| null lhs
= error $
printf "splitEvents: null lhs: len = %d, startTime = %d, endTime = %d\n"
(length es) startTime endTime
++ '\n': show es
| otherwise
= -- trace (printf "len = %d, startTime = %d, endTime = %d, lhs_len = %d\n" len startTime endTime lhs_len) $
assert (length lhs + length rhs == length es) $
EventSplit (evTime (head rhs))
ltree
rtree
where
-- | Integer division, rounding up.
divUp :: Timestamp -> Timestamp -> Timestamp
divUp n k = (n + k - 1) `div` k
startTime = evTime (head es)
splitTime = startTime + (endTime - startTime) `divUp` 2
duration = endTime - startTime
(lhs, lhs_end, rhs) = splitEventList es [] splitTime 0
ltree = splitEvents lhs lhs_end
rtree = splitEvents rhs endTime
splitEventList :: [GHC.Event]
-> [GHC.Event]
-> Timestamp
-> Timestamp
-> ([GHC.Event], Timestamp, [GHC.Event])
splitEventList [] acc !_tsplit !tmax
= (reverse acc, tmax, [])
splitEventList [e] acc !_tsplit !tmax
-- Just one event left: put it on the right. This ensures that we
-- have at least one event on each side of the split.
= (reverse acc, tmax, [e])
splitEventList (e:es) acc !tsplit !tmax
| t <= tsplit -- pick all events that start at or before the split
= splitEventList es (e:acc) tsplit (max tmax t)
| otherwise
= (reverse acc, tmax, e:es)
where
t = evTime e
-------------------------------------------------------------------------------
reportEventTree :: Int -> EventTree -> IO ()
reportEventTree hecNumber (EventTree _ _ eventTree)
= putStrLn ("HEC " ++ show hecNumber ++ reportText)
where
reportText = " nodes = " ++ show (eventTreeCountNodes eventTree) ++
" max depth = " ++ show (eventNodeMaxDepth eventTree)
-------------------------------------------------------------------------------
eventTreeCountNodes :: EventNode -> Int
eventTreeCountNodes (EventSplit _ lhs rhs)
= 1 + eventTreeCountNodes lhs + eventTreeCountNodes rhs
eventTreeCountNodes _ = 1
-------------------------------------------------------------------------------
eventTreeMaxDepth :: EventTree -> Int
eventTreeMaxDepth (EventTree _ _ t) = eventNodeMaxDepth t
eventNodeMaxDepth :: EventNode -> Int
eventNodeMaxDepth (EventSplit _ lhs rhs)
= 1 + eventNodeMaxDepth lhs `max` eventNodeMaxDepth rhs
eventNodeMaxDepth _ = 1
================================================
FILE: Events/HECs.hs
================================================
{-# LANGUAGE CPP #-}
module Events.HECs (
HECs(..),
Event,
Timestamp,
eventIndexToTimestamp,
timestampToEventIndex,
extractUserMarkers,
histogram,
histogramCounts,
) where
import Events.EventTree
import Events.SparkTree
import GHC.RTS.Events
import Data.Array
import Data.Text (Text)
import qualified Data.List as L
#if MIN_VERSION_containers(0,5,0)
import qualified Data.IntMap.Strict as IM
#else
import qualified Data.IntMap as IM
#endif
-----------------------------------------------------------------------------
-- all the data from a .eventlog file
data HECs = HECs {
hecCount :: Int,
hecTrees :: [(DurationTree, EventTree, SparkTree)],
hecEventArray :: Array Int Event,
hecLastEventTime :: Timestamp,
maxSparkPool :: Double,
minXHistogram :: Int,
maxXHistogram :: Int,
maxYHistogram :: Timestamp,
durHistogram :: [(Timestamp, Int, Timestamp)],
perfNames :: IM.IntMap Text
}
-----------------------------------------------------------------------------
eventIndexToTimestamp :: HECs -> Int -> Timestamp
eventIndexToTimestamp HECs{hecEventArray=arr} n =
evTime (arr ! n)
timestampToEventIndex :: HECs -> Timestamp -> Int
timestampToEventIndex HECs{hecEventArray=arr} ts =
search l (r+1)
where
(l,r) = bounds arr
search !l !r
| (r - l) <= 1 = if ts > evTime (arr!l) then r else l
| ts < tmid = search l mid
| otherwise = search mid r
where
mid = l + (r - l) `quot` 2
tmid = evTime (arr!mid)
extractUserMarkers :: HECs -> [(Timestamp, Text)]
extractUserMarkers hecs =
[ (ts, mark)
| (Event ts (UserMarker mark) _) <- elems (hecEventArray hecs) ]
-- | Sum durations in the same buckets to form a histogram.
histogram :: [(Int, Timestamp)] -> [(Int, Timestamp)]
histogram durs = IM.toList $ fromListWith' (+) durs
-- | Sum durations and spark counts in the same buckets to form a histogram.
histogramCounts :: [(Int, (Timestamp, Int))] -> [(Int, (Timestamp, Int))]
histogramCounts durs =
let agg (dur1, count1) (dur2, count2) =
-- bangs needed to avoid stack overflow
let !dur = dur1 + dur2
!count = count1 + count2
in (dur, count)
in IM.toList $ fromListWith' agg durs
fromListWith' :: (a -> a -> a) -> [(Int, a)] -> IM.IntMap a
fromListWith' f xs =
L.foldl' ins IM.empty xs
where
#if MIN_VERSION_containers(0,5,0)
ins t (k,x) = IM.insertWith f k x t
#elif MIN_VERSION_containers(0,4,1)
ins t (k,x) = IM.insertWith' f k x t
#else
ins t (k,x) =
let r = IM.insertWith f k x t
v = r IM.! k
in v `seq` r
#endif
================================================
FILE: Events/ReadEvents.hs
================================================
module Events.ReadEvents (
registerEventsFromFile, registerEventsFromTrace
) where
import Events.EventDuration
import Events.EventTree
import Events.HECs (HECs (..), histogram)
import Events.SparkTree
import Events.TestEvents
import GUI.ProgressView (ProgressView)
import qualified GUI.ProgressView as ProgressView
import GHC.RTS.Events
import GHC.RTS.Events.Analysis
import GHC.RTS.Events.Analysis.Capability
import GHC.RTS.Events.Analysis.SparkThread
import qualified Control.DeepSeq as DeepSeq
import Control.Exception
import Control.Monad
import Data.Array
import Data.Either
import Data.Function
import qualified Data.IntMap as IM
import qualified Data.List as L
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe)
import Data.Set (Set)
import System.FilePath
import Text.Printf
-------------------------------------------------------------------------------
-- import qualified GHC.RTS.Events as GHCEvents
--
-- The GHC.RTS.Events library returns the profile information
-- in a data-structure which contains a list data structure
-- representing the events i.e. [GHCEvents.Event]
-- ThreadScope transforms this list into an alternative representation
-- which (for each HEC) records event *durations* which are ordered in time.
-- The durations represent the run-lengths for thread execution and
-- run-lengths for garbage collection. This data-structure is called
-- EventDuration.
-- ThreadScope then transformations this data-structure into another
-- data-structure which gives a binary-tree view of the event information
-- by performing a binary split on the time domain i.e. the EventTree
-- data structure.
-- GHCEvents.Event => [EventDuration] => EventTree
-------------------------------------------------------------------------------
rawEventsToHECs :: [Event] -> Timestamp
-> [(Double, (DurationTree, EventTree, SparkTree))]
rawEventsToHECs evs endTime
= map (\cap -> toTree $ L.find ((Just cap ==) . evCap . head) heclists)
[0 .. maximum (0 : map (fromMaybe 0 . evCap) evs)]
where
heclists =
L.groupBy ((==) `on` evCap) $ L.sortBy (compare `on` evCap) evs
toTree Nothing = (0, (DurationTreeEmpty,
EventTree 0 0 (EventTreeLeaf []),
emptySparkTree))
toTree (Just evs) =
(maxSparkPool,
(mkDurationTree (eventsToDurations nondiscrete) endTime,
mkEventTree discrete endTime,
mkSparkTree sparkD endTime))
where (discrete, nondiscrete) = L.partition isDiscreteEvent evs
(maxSparkPool, sparkD) = eventsToSparkDurations nondiscrete
-------------------------------------------------------------------------------
registerEventsFromFile :: String -> ProgressView
-> IO (HECs, String, Int, Double)
registerEventsFromFile filename = registerEvents (Left filename)
registerEventsFromTrace :: String -> ProgressView
-> IO (HECs, String, Int, Double)
registerEventsFromTrace traceName = registerEvents (Right traceName)
registerEvents :: Either FilePath String
-> ProgressView
-> IO (HECs, String, Int, Double)
registerEvents from progress = do
let msg = case from of
Left filename -> filename
Right test -> test
ProgressView.setTitle progress ("Loading " ++ takeFileName msg)
buildEventLog progress from
-------------------------------------------------------------------------------
-- Runs in a background thread
--
buildEventLog :: ProgressView -> Either FilePath String
-> IO (HECs, String, Int, Double)
buildEventLog progress from =
case from of
Right test -> build test (testTrace test)
Left filename -> do
stopPulse <- ProgressView.startPulse progress
fmt <- readEventLogFromFile filename
stopPulse
case fmt of
Left err -> fail err --FIXME: report error properly
Right evs -> build filename evs
where
-- | Integer division, rounding up.
divUp :: Timestamp -> Timestamp -> Timestamp
divUp n k = (n + k - 1) `div` k
build name evs = do
let
eBy1000 ev = ev{evTime = evTime ev `divUp` 1000}
eventsBy = map eBy1000 (events (dat evs))
eventBlockEnd e | EventBlock{ end_time=t } <- evSpec e = t
eventBlockEnd e = evTime e
-- 1, to avoid graph scale 0 and division by 0 later on
lastTx = maximum (1 : map eventBlockEnd eventsBy)
-- Add caps to perf events, using the OS thread numbers
-- obtained from task validation data.
-- Only the perf events with a cap are displayed in the timeline.
-- TODO: it may make sense to move this code to ghc-events
-- and run after to-eventlog and ghc-events merge, but it requires
-- one more step in the 'perf to TS' workflow and is a bit slower
-- (yet another event sorting and loading eventlog chunks
-- into the CPU cache).
steps :: [Event] -> [(Map KernelThreadId Int, Event)]
steps evs =
zip (map fst $ rights $ validates capabilityTaskOSMachine evs) evs
addC :: (Map KernelThreadId Int, Event) -> Event
addC (state, ev@Event{evSpec=PerfTracepoint{tid}}) =
case M.lookup tid state of
Nothing -> ev -- unknown task's OS thread
evCap -> ev {evCap}
addC (state, ev@Event{evSpec=PerfCounter{tid}}) =
case M.lookup tid state of
Nothing -> ev -- unknown task's OS thread
evCap -> ev {evCap}
addC (_, ev) = ev
addCaps evs = map addC (steps evs)
-- sort the events by time, add extra caps and put them in an array
sorted = addCaps $ sortEvents eventsBy
maxTrees = rawEventsToHECs sorted lastTx
maxSparkPool = maximum (0 : map fst maxTrees)
trees = map snd maxTrees
-- put events in an array
n_events = length sorted
event_arr = listArray (0, n_events-1) sorted
hec_count = length trees
-- Pre-calculate the data for the sparks histogram.
intDoub :: Integral a => a -> Double
intDoub = fromIntegral
-- Discretizes the data using log.
-- Log base 2 seems to result in 7--15 bars, which is OK visually.
-- Better would be 10--15 bars, but we want the base to be a small
-- integer, for readable scales, and we can't go below 2.
ilog :: Timestamp -> Int
ilog 0 = 0
ilog x = floor $ logBase 2 (intDoub x)
times :: (Int, Timestamp, Timestamp)
-> Maybe (Timestamp, Int, Timestamp)
times (_, timeStarted, timeElapsed) =
Just (timeStarted, ilog timeElapsed, timeElapsed)
sparkProfile :: Process
((Map ThreadId (Profile SparkThreadState),
(Map Int ThreadId, Set ThreadId)),
Event)
(ThreadId, (SparkThreadState, Timestamp, Timestamp))
sparkProfile = profileRouted
(refineM evSpec sparkThreadMachine)
capabilitySparkThreadMachine
capabilitySparkThreadIndexer
evTime
sorted
sparkSummary :: Map ThreadId (Int, Timestamp, Timestamp)
-> [(ThreadId, (SparkThreadState, Timestamp, Timestamp))]
-> [Maybe (Timestamp, Int, Timestamp)]
sparkSummary m [] = map times $ M.elems m
sparkSummary m ((threadId, (state, timeStarted', timeElapsed')):xs) =
case state of
SparkThreadRunning sparkId' -> case M.lookup threadId m of
Just el@(sparkId, timeStarted, timeElapsed) ->
if sparkId == sparkId'
then let value = (sparkId, timeStarted, timeElapsed + timeElapsed')
in sparkSummary (M.insert threadId value m) xs
else times el : newSummary sparkId' xs
Nothing -> newSummary sparkId' xs
_ -> sparkSummary m xs
where
newSummary sparkId = let value = (sparkId, timeStarted', timeElapsed')
in sparkSummary (M.insert threadId value m)
allHisto :: [(Timestamp, Int, Timestamp)]
allHisto = catMaybes . sparkSummary M.empty . toList $ sparkProfile
-- Sparks of zero length are already well visualized in other graphs:
durHistogram = filter (\ (_, logdur, _) -> logdur > 0) allHisto
-- Precompute some extremums of the maximal interval, needed for scales.
durs = [(logdur, dur) | (_start, logdur, dur) <- durHistogram]
(logDurs, sumDurs) = L.unzip (histogram durs)
minXHistogram = minimum (maxBound : logDurs)
maxXHistogram = maximum (minBound : logDurs)
maxY = maximum (minBound : sumDurs)
-- round up to multiples of 10ms
maxYHistogram = 10000 * ceiling (fromIntegral maxY / 10000)
getPerfNames nmap ev =
case evSpec ev of
PerfName{perfNum, name} ->
IM.insert (fromIntegral perfNum) name nmap
_ -> nmap
perfNames = L.foldl' getPerfNames IM.empty eventsBy
hecs = HECs {
hecCount = hec_count,
hecTrees = trees,
hecEventArray = event_arr,
hecLastEventTime = lastTx,
maxSparkPool,
minXHistogram,
maxXHistogram,
maxYHistogram,
durHistogram,
perfNames
}
treeProgress :: Int -> (DurationTree, EventTree, SparkTree) -> IO ()
treeProgress hec (tree1, tree2, tree3) = do
ProgressView.setText progress $
printf "Building HEC %d/%d" (hec+1) hec_count
ProgressView.setProgress progress hec_count hec
evaluate tree1
evaluate (eventTreeMaxDepth tree2)
evaluate (sparkTreeMaxDepth tree3)
when (hec_count == 1 || hec == 1) -- eval only with 2nd HEC
(return $! DeepSeq.rnf durHistogram)
zipWithM_ treeProgress [0..] trees
ProgressView.setProgress progress hec_count hec_count
-- TODO: fully evaluate HECs before returning because otherwise the last
-- bit of work gets done after the progress window has been closed.
return (hecs, name, n_events, fromIntegral lastTx / 1000000)
================================================
FILE: Events/SparkStats.hs
================================================
module Events.SparkStats
( SparkStats(..)
, initial, create, rescale, aggregate, agEx
) where
import Data.Word (Word64)
-- | Sparks change state. Each state transition process has a duration.
-- Spark statistics, for a given duration, record the spark transition rate
-- (the number of sparks that enter a given state within the interval)
-- and the absolute mean, maximal and minimal number of sparks
-- in the spark pool within the duration.
data SparkStats =
SparkStats { rateCreated, rateDud, rateOverflowed,
rateConverted, rateFizzled, rateGCd,
meanPool, maxPool, minPool :: {-# UNPACK #-}!Double }
deriving (Show, Eq)
-- | Initial, default value of spark stats, at the start of runtime,
-- before any spark activity is recorded.
initial :: SparkStats
initial = SparkStats 0 0 0 0 0 0 0 0 0
-- | Create spark stats for a duration, given absolute
-- numbers of sparks in all categories at the start and end of the duration.
-- The units for spark transitions (first 6 counters) is [spark/duration]:
-- the fact that intervals may have different lengths is ignored here.
-- The units for the pool stats are just [spark].
-- The values in the second counter have to be greater or equal
-- to the values in the first counter, except for the spark pool size.
-- For pool size, we take into account only the first sample,
-- to visualize more detail at high zoom levels, at the cost
-- of a slight shift of the graph. Mathematically, this corresponds
-- to taking the initial durations as centered around samples,
-- but to have the same tree for rates and pool sizes, we then have
-- to shift the durations by half interval size to the right
-- (which would be neglectable if the interval was small and even).
create :: (Word64, Word64, Word64, Word64, Word64, Word64, Word64)
-> (Word64, Word64, Word64, Word64, Word64, Word64, Word64)
-> SparkStats
create (crt1, dud1, ovf1, cnv1, fiz1, gcd1, remaining1)
(crt2, dud2, ovf2, cnv2, fiz2, gcd2, _remaining2) =
let (crt, dud, ovf, cnv, fiz, gcd) =
(fromIntegral $ crt2 - crt1,
fromIntegral $ dud2 - dud1,
fromIntegral $ ovf2 - ovf1,
fromIntegral $ cnv2 - cnv1,
fromIntegral $ fiz2 - fiz1,
fromIntegral $ gcd2 - gcd1)
p = fromIntegral remaining1
in SparkStats crt dud ovf cnv fiz gcd p p p
-- | Reduce a list of spark stats; spark pool stats are overwritten.
foldStats :: (Double -> Double -> Double)
-> Double -> Double -> Double
-> [SparkStats] -> SparkStats
foldStats f meanP maxP minP l
= SparkStats
(foldr f 0 (map rateCreated l))
(foldr f 0 (map rateDud l))
(foldr f 0 (map rateOverflowed l))
(foldr f 0 (map rateConverted l))
(foldr f 0 (map rateFizzled l))
(foldr f 0 (map rateGCd l))
meanP maxP minP
-- | Rescale the spark transition stats, e.g., to change their units.
rescale :: Double -> SparkStats -> SparkStats
rescale scale s =
let f w _ = scale * w
in foldStats f (meanPool s) (maxPool s) (minPool s) [s]
-- | Derive spark stats for an interval from a list of spark stats,
-- in reverse chronological order, of consecutive subintervals
-- that sum up to the original interval.
aggregate :: [SparkStats] -> SparkStats
aggregate [] = error "aggregate"
aggregate [s] = s -- optimization
aggregate l =
let meanP = sum (map meanPool l) / fromIntegral (length l) -- TODO: inaccurate
maxP = maximum (map maxPool l)
minP = minimum (map minPool l)
in foldStats (+) meanP maxP minP l
-- | Extrapolate spark stats from previous data.
-- Absolute pools size values extrapolate by staying constant,
-- rates of change of spark status extrapolate by dropping to 0
-- (which corresponds to absolute numbers of sparks staying constant).
extrapolate :: SparkStats -> SparkStats
extrapolate s =
let f w _ = 0 * w
in foldStats f (meanPool s) (maxPool s) (minPool s) [s]
-- | Aggregate, if any data provided. Extrapolate from previous data, otherwise.
-- In both cases, the second component is the new choice of "previous data".
-- The list of stats is expected in reverse chronological order,
-- as for aggregate.
agEx :: [SparkStats] -> SparkStats -> (SparkStats, SparkStats)
agEx [] s = (extrapolate s, s)
agEx l@(s:_) _ = (aggregate l, s)
================================================
FILE: Events/SparkTree.hs
================================================
module Events.SparkTree (
SparkTree,
sparkTreeMaxDepth,
emptySparkTree,
eventsToSparkDurations,
mkSparkTree,
sparkProfile,
) where
import qualified Events.SparkStats as SparkStats
import GHC.RTS.Events (Timestamp)
import qualified GHC.RTS.Events as GHCEvents
import Control.Exception (assert)
import Text.Printf
-- import Debug.Trace
-- | Sparks change state. Each state transition process has a duration.
-- SparkDuration is a condensed description of such a process,
-- containing a start time of the duration interval,
-- spark stats that record the spark transition rate
-- and the absolute number of sparks in the spark pool within the duration.
data SparkDuration =
SparkDuration { startT :: {-#UNPACK#-}!Timestamp,
deltaC :: {-#UNPACK#-}!SparkStats.SparkStats }
deriving Show
-- | Calculates durations and maximal rendered values from the event log.
-- Warning: cannot be applied to a suffix of the log (assumes start at time 0).
eventsToSparkDurations :: [GHCEvents.Event] -> (Double, [SparkDuration])
eventsToSparkDurations es =
let aux _startTime _startCounters [] = (0, [])
aux startTime startCounters (event : events) =
case GHCEvents.evSpec event of
GHCEvents.SparkCounters crt dud ovf cnv fiz gcd rem ->
let endTime = GHCEvents.evTime event
endCounters = (crt, dud, ovf, cnv, fiz, gcd, rem)
delta = SparkStats.create startCounters endCounters
newMaxSparkPool = SparkStats.maxPool delta
sd = SparkDuration { startT = startTime,
deltaC = delta }
(oldMaxSparkPool, l) = aux endTime endCounters events
in (max oldMaxSparkPool newMaxSparkPool, sd : l)
_otherEvent -> aux startTime startCounters events
in aux 0 (0,0,0,0,0,0,0) es
-- | We map the spark transition durations (intervals) onto a binary
-- search tree, so that we can easily find the durations
-- that correspond to a particular view of the timeline.
-- Additionally, each node of the tree contains a summary
-- of the information below it, so that we can render views at various
-- levels of resolution. For example, if a tree node would represent
-- less than one pixel on the display, there is no point is descending
-- the tree further.
data SparkTree
= SparkTree
{-#UNPACK#-}!Timestamp -- ^ start time of span represented by the tree
{-#UNPACK#-}!Timestamp -- ^ end time of the span represented by the tree
SparkNode
deriving Show
data SparkNode
= SparkSplit
{-#UNPACK#-}!Timestamp -- ^ time used to split the span into two parts
SparkNode
-- ^ the LHS split; all data lies completely between start and split
SparkNode
-- ^ the RHS split; all data lies completely between split and end
{-#UNPACK#-}!SparkStats.SparkStats
-- ^ aggregate of the spark stats within the span
| SparkTreeLeaf
{-#UNPACK#-}!SparkStats.SparkStats
-- ^ the spark stats for the base duration
| SparkTreeEmpty
-- ^ represents a span that no data referts to, e.g., after the last GC
deriving Show
sparkTreeMaxDepth :: SparkTree -> Int
sparkTreeMaxDepth (SparkTree _ _ t) = sparkNodeMaxDepth t
sparkNodeMaxDepth :: SparkNode -> Int
sparkNodeMaxDepth (SparkSplit _ lhs rhs _)
= 1 + sparkNodeMaxDepth lhs `max` sparkNodeMaxDepth rhs
sparkNodeMaxDepth _ = 1
emptySparkTree :: SparkTree
emptySparkTree = SparkTree 0 0 SparkTreeEmpty
-- | Create spark tree from spark durations.
-- Note that the last event may be not a spark event, in which case
-- there is no data about sparks for the last time interval
-- (the subtree for the interval will have SparkTreeEmpty node).
mkSparkTree :: [SparkDuration] -- ^ spark durations calculated from events
-> Timestamp -- ^ end time of last event in the list
-> SparkTree
mkSparkTree es endTime =
SparkTree s e $
-- trace (show tree) $
tree
where
tree = splitSparks es endTime
(s, e) = if null es then (0, 0) else (startT (head es), endTime)
-- | Construct spark tree, by recursively splitting time intervals..
-- We only split at spark transition duration boundaries;
-- we never split a duration into multiple pieces.
-- Therefore, the binary tree is only roughly split by time,
-- the actual split depends on the distribution of sample points below it.
splitSparks :: [SparkDuration] -> Timestamp -> SparkNode
splitSparks [] !_endTime =
SparkTreeEmpty
splitSparks [e] !_endTime =
SparkTreeLeaf (deltaC e)
splitSparks es !endTime
| null rhs
= splitSparks es lhs_end
| null lhs
= error $
printf "splitSparks: null lhs: len = %d, startTime = %d, endTime = %d\n"
(length es) startTime endTime
++ '\n' : show es
| otherwise
= -- trace (printf "len = %d, startTime = %d, endTime = %d\n" (length es) startTime endTime) $
assert (length lhs + length rhs == length es) $
SparkSplit (startT $ head rhs)
ltree
rtree
(SparkStats.aggregate (subDelta rtree ++ subDelta ltree))
where
-- | Integer division, rounding up.
divUp :: Timestamp -> Timestamp -> Timestamp
divUp n k = (n + k - 1) `div` k
startTime = startT $ head es
splitTime = startTime + (endTime - startTime) `divUp` 2
(lhs, lhs_end, rhs) = splitSparkList es [] splitTime 0
ltree = splitSparks lhs lhs_end
rtree = splitSparks rhs endTime
subDelta (SparkSplit _ _ _ delta) = [delta]
subDelta (SparkTreeLeaf delta) = [delta]
subDelta SparkTreeEmpty = []
splitSparkList :: [SparkDuration]
-> [SparkDuration]
-> Timestamp
-> Timestamp
-> ([SparkDuration], Timestamp, [SparkDuration])
splitSparkList [] acc !_tsplit !tmax
= (reverse acc, tmax, [])
splitSparkList [e] acc !_tsplit !tmax
-- Just one event left: put it on the right. This ensures that we
-- have at least one event on each side of the split.
= (reverse acc, tmax, [e])
splitSparkList (e:es) acc !tsplit !tmax
| startT e <= tsplit -- pick all durations that start at or before the split
= splitSparkList es (e:acc) tsplit (max tmax (startT e))
| otherwise
= (reverse acc, tmax, e:es)
-- | For each timeslice, give the spark stats calculated for that interval.
-- The spark stats are Approximated from the aggregated data
-- at the level of the spark tree covering intervals of the size
-- similar to the timeslice size.
sparkProfile :: Timestamp -> Timestamp -> Timestamp -> SparkTree
-> [SparkStats.SparkStats]
sparkProfile slice start0 end0 t
= {- trace (show flat) $ -} chopped
where
-- do an extra slice at both ends
start = if start0 < slice then start0 else start0 - slice
end = end0 + slice
flat = flatten start t []
-- TODO: redefine chop so that it's obvious this error will not happen
-- e.g., catch pathological cases, like a tree with only SparkTreeEmpty
-- inside and/or make it tail-recursive instead of
-- taking the 'previous' argument
chopped0 = chop (error "Fatal error in sparkProfile.") [] start flat
chopped | start0 < slice = SparkStats.initial : chopped0
| otherwise = chopped0
flatten :: Timestamp -> SparkTree -> [SparkTree] -> [SparkTree]
flatten _start (SparkTree _s _e SparkTreeEmpty) rest = rest
flatten start t@(SparkTree s e (SparkSplit split l r _)) rest
| e <= start = rest
| end <= s = rest
| start >= split = flatten start (SparkTree split e r) rest
| end <= split = flatten start (SparkTree s split l) rest
| e - s > slice = flatten start (SparkTree s split l) $
flatten start (SparkTree split e r) rest
-- A rule of thumb: if a node is narrower than slice, don't drill down,
-- even if the node sits astride slice boundaries and so the readings
-- for each of the two neigbouring slices will not be accurate
-- (but for the pair as a whole, they will be). Smooths the curve down
-- even more than averaging over the timeslice already does.
| otherwise = t : rest
flatten _start t@(SparkTree _s _e (SparkTreeLeaf _)) rest
= t : rest
chop :: SparkStats.SparkStats -> [SparkStats.SparkStats]
-> Timestamp -> [SparkTree] -> [SparkStats.SparkStats]
chop _previous sofar start1 _ts
| start1 >= end
= case sofar of
_ : _ -> [SparkStats.aggregate sofar]
[] -> []
chop _previous sofar _start1 [] -- data too short for the redrawn area
| null sofar -- no data at all in the redrawn area
= []
| otherwise
= [SparkStats.aggregate sofar]
chop previous sofar start1 (t : ts)
| e <= start1 -- skipping data left of the slice
= case sofar of
_ : _ -> error "chop"
[] -> chop previous sofar start1 ts
| s >= start1 + slice -- postponing data right of the slice
= let (c, p) = SparkStats.agEx sofar previous
in c : chop p [] (start1 + slice) (t : ts)
| e > start1 + slice
= let (c, p) = SparkStats.agEx (created_in_this_slice t ++ sofar) previous
in c : chop p [] (start1 + slice) (t : ts)
| otherwise
= chop previous (created_in_this_slice t ++ sofar) start1 ts
where
(s, e) | SparkTree s e _ <- t = (s, e)
-- The common part of the slice and the duration.
mi = min (start1 + slice) e
ma = max start1 s
common = if mi < ma then 0 else mi - ma
-- Instead of drilling down the tree (unless it's a leaf),
-- we approximate by taking a proportion of the aggregate value,
-- depending on how much of the spark duration corresponding
-- to the tree node is covered by our timeslice.
proportion = if e > s
then fromIntegral common / fromIntegral (e - s)
else assert (e == s && common == 0) $ 0
-- Spark transitions in the tree are in units spark/duration.
-- Here the numbers are rescaled so that the units are spark/ms.
created_in_this_slice (SparkTree _ _ node) = case node of
SparkTreeLeaf delta -> [SparkStats.rescale proportion delta]
SparkTreeEmpty -> []
SparkSplit _ _ _ delta -> [SparkStats.rescale proportion delta]
================================================
FILE: Events/TestEvents.hs
================================================
{-# LANGUAGE OverloadedStrings #-}
module Events.TestEvents (testTrace)
where
import Data.Word
import GHC.RTS.Events
-------------------------------------------------------------------------------
testTrace :: String -> EventLog
testTrace name = eventLog (test name)
-------------------------------------------------------------------------------
eventLog :: [Event] -> EventLog
eventLog events =
let eBy1000 ev = ev{evTime = evTime ev * 1000}
eventsBy = map eBy1000 events
in EventLog (Header testEventTypes) (Data eventsBy)
-------------------------------------------------------------------------------
create :: Word16
create = 0
-------------------------------------------------------------------------------
runThread :: Word16
runThread = 1
-------------------------------------------------------------------------------
stop :: Word16
stop = 2
-------------------------------------------------------------------------------
runnable :: Word16
runnable = 3
-------------------------------------------------------------------------------
migrate :: Word16
migrate = 4
-------------------------------------------------------------------------------
runSpark :: Word16
runSpark = 5
-------------------------------------------------------------------------------
stealSpark :: Word16
stealSpark = 6
-------------------------------------------------------------------------------
shutdown :: Word16
shutdown = 7
-------------------------------------------------------------------------------
wakeup :: Word16
wakeup = 8
-------------------------------------------------------------------------------
startGC :: Word16
startGC = 9
------------------------------------------------------------------------------
finishGC :: Word16
finishGC = 10
------------------------------------------------------------------------------
reqSeqGC :: Word16
reqSeqGC = 11
------------------------------------------------------------------------------
reqParGC :: Word16
reqParGC = 12
------------------------------------------------------------------------------
createSparkThread :: Word16
createSparkThread = 15
------------------------------------------------------------------------------
logMessage :: Word16
logMessage = 16
------------------------------------------------------------------------------
startup :: Word16
startup = 17
------------------------------------------------------------------------------
blockMarker :: Word16
blockMarker = 18
------------------------------------------------------------------------------
testEventTypes :: [EventType]
testEventTypes
= [EventType create "Create thread" (Just 8),
EventType runThread "Run thread" (Just 8),
EventType stop "Stop thread" (Just 10),
EventType runnable "Thread runnable" (Just 8),
EventType migrate "Migrate thread" (Just 10),
EventType runSpark "Run spark" (Just 8),
EventType stealSpark "Steal spark" (Just 10),
EventType shutdown "Shutdown" (Just 0),
EventType wakeup "Wakeup thread" (Just 10),
EventType startGC "Start GC" (Just 0),
EventType finishGC "Finish GC" (Just 0),
EventType reqSeqGC "Request sequential GC" (Just 0),
EventType reqParGC "Reqpargc parallel GC" (Just 0),
EventType createSparkThread "Create spark thread" (Just 8),
EventType logMessage "Log message" Nothing,
EventType startup "Startup" (Just 0),
EventType blockMarker "Block marker" (Just 14)
]
-------------------------------------------------------------------------------
test :: String -> [Event]
-------------------------------------------------------------------------------
test "empty0"
= [
Event 0 (Startup 1) (Just 0)
]
-------------------------------------------------------------------------------
test "empty1"
= [
Event 0 (Startup 1) (Just 0)
]
-------------------------------------------------------------------------------
test "test0"
= [
Event 0 (Startup 1) (Just 0),
Event 4000000 Shutdown (Just 0)
]
-------------------------------------------------------------------------------
test "small"
= [
Event 0 (Startup 1) (Just 0),
Event 1000000 (CreateThread 1) (Just 0),
Event 2000000 (RunThread 1) (Just 0),
Event 3000000 (StopThread 1 ThreadFinished) (Just 0),
Event 4000000 (Shutdown) (Just 0)
]
-------------------------------------------------------------------------------
test "tick"
= [-- A thread from 2s to 3s
Event 0 (Startup 3) (Just 0),
Event 1000000000 (CreateThread 1) (Just 0),
Event 2000000000 (RunThread 1) (Just 0),
Event 3000000000 (StopThread 1 ThreadFinished) (Just 0),
Event 4000000000 (Shutdown) (Just 0),
-- A thread from 0.2ms to 0.3ms
Event 1000000 (CreateThread 2) (Just 1),
Event 2000000 (RunThread 2) (Just 1),
Event 3000000 (StopThread 2 ThreadFinished) (Just 1),
Event 4000000 (Shutdown) (Just 1),
-- A thread from 0.2us to 0.3us
Event 1000 (CreateThread 3) (Just 2),
Event 2000 (RunThread 3) (Just 2),
Event 3000 (StopThread 3 ThreadFinished) (Just 2),
Event 4000 (Shutdown) (Just 2)
]
-------------------------------------------------------------------------------
test "tick2"
= [-- A thread create but no run
Event 0 (Startup 1) (Just 0),
Event 1000000000 (CreateThread 1) (Just 0),
Event 4000000000 (Shutdown) (Just 0)
]
-------------------------------------------------------------------------------
test "tick3"
= [-- A thread from 2s to 3s
Event 0 (Startup 1) (Just 0),
Event 1000000000 (CreateThread 1) (Just 0),
Event 2000000000 (RunThread 1) (Just 0),
Event 3000000000 (StopThread 1 ThreadFinished) (Just 0),
Event 4000000000 (Shutdown) (Just 0)
]
-------------------------------------------------------------------------------
test "tick4"
= [-- A test for scale values close to 1.0
Event 0 (Startup 1) (Just 0),
Event 100 (CreateThread 1) (Just 0),
Event 200 (RunThread 1) (Just 0),
Event 300 (StopThread 1 ThreadFinished) (Just 0),
Event 400 (Shutdown) (Just 0)
]
-------------------------------------------------------------------------------
test "tick5"
= [-- A thread from 2s to 3s
Event 0 (Startup 1) (Just 0),
Event 1000000000 (CreateThread 1) (Just 0),
Event 2000000000 (RunThread 1) (Just 0),
Event 3000000000 (StopThread 1 ThreadFinished) (Just 0),
Event 4000000000 (Shutdown) (Just 0)
]
-------------------------------------------------------------------------------
-- A long tick run to check small and large tick labels
test "tick6" = chequered 2 100 10000000
-------------------------------------------------------------------------------
test "overlap"
= [-- A thread from 2s to 3s
Event 0 (Startup 1) (Just 0),
Event 1000 (CreateThread 1) (Just 0),
Event 1100 (RunThread 1) (Just 0),
Event 1200 (CreateThread 2) (Just 0),
Event 1300 (StopThread 1 ThreadFinished) (Just 0),
Event 1400 (RunThread 2) (Just 0),
Event 1500 (CreateThread 3) (Just 0),
Event 1500 (CreateThread 4) (Just 0),
Event 1500 (StopThread 2 ThreadFinished) (Just 0),
Event 1600 (RunThread 3) (Just 0),
Event 1600 (CreateThread 5) (Just 0),
Event 1600 (StopThread 3 ThreadFinished) (Just 0),
Event 1700 (RunThread 4) (Just 0),
Event 1700 (CreateThread 6) (Just 0),
Event 1800 (StopThread 4 ThreadFinished) (Just 0),
Event 3000 (Shutdown) (Just 0)
]
-------------------------------------------------------------------------------
-- These tests are for chequered patterns to help check for rendering
-- problems and also to help test the performance of scrolling etc.
-- Each line has a fixed frequency of a thread running and then performing GC.
-- Each successive HEC runs thread at half the frequency of the previous HEC.
test "ch1" = chequered 1 100 100000
test "ch2" = chequered 2 100 100000
test "ch3" = chequered 3 100 100000
test "ch4" = chequered 4 100 100000
test "ch5" = chequered 5 100 100000
test "ch6" = chequered 6 100 100000
test "ch7" = chequered 7 100 100000
test "ch8" = chequered 8 100 100000
-------------------------------------------------------------------------------
test _ = []
-------------------------------------------------------------------------------
chequered :: ThreadId -> Timestamp -> Timestamp -> [Event]
chequered numThreads basicDuration runLength
= Event 0 (Startup (fromIntegral numThreads)) (Just 0) :
makeChequered 1 numThreads basicDuration runLength
-------------------------------------------------------------------------------
makeChequered :: ThreadId -> ThreadId -> Timestamp -> Timestamp -> [Event]
makeChequered currentThread numThreads _basicDuration _runLength
| currentThread > numThreads = [] -- All threads rendered
makeChequered currentThread numThreads basicDuration runLength
= eventBlock ++
makeChequered (currentThread+1) numThreads (2*basicDuration) runLength
where
eventBlock = Event 0 (CreateThread currentThread) (Just $ fromIntegral $ currentThread - 1)
: chequeredPattern currentThread 0 basicDuration runLength
-------------------------------------------------------------------------------
chequeredPattern :: ThreadId -> Timestamp -> Timestamp -> Timestamp -> [Event]
chequeredPattern currentThread currentPos basicDuration runLength
= if currentPos + 2*basicDuration > runLength then
[Event runLength Shutdown mcap]
else
[Event currentPos (RunThread currentThread) mcap,
Event (currentPos+basicDuration) (StopThread currentThread ThreadYielding) mcap,
Event (currentPos+basicDuration) StartGC mcap,
Event (currentPos+2*basicDuration) EndGC mcap
] ++ chequeredPattern currentThread (currentPos+2*basicDuration) basicDuration runLength
where mcap = Just $ fromIntegral $ currentThread - 1
-------------------------------------------------------------------------------
================================================
FILE: GUI/App.hs
================================================
-------------------------------------------------------------------------------
-- | Module : GUI.App
--
-- Platform-specific application functionality
-------------------------------------------------------------------------------
module GUI.App (initApp) where
-------------------------------------------------------------------------------
-- | Initialize application
-- Perform application initialization for non-macOS platforms
initApp :: IO ()
initApp = return ()
================================================
FILE: GUI/BookmarkView.hs
================================================
module GUI.BookmarkView (
BookmarkView,
bookmarkViewNew,
BookmarkViewActions(..),
bookmarkViewGet,
bookmarkViewAdd,
bookmarkViewRemove,
bookmarkViewClear,
bookmarkViewSetLabel,
) where
import GHC.RTS.Events (Timestamp)
import Graphics.UI.Gtk
import qualified Graphics.UI.Gtk.ModelView.TreeView.Compat as Compat
import Numeric
import Data.Text (Text)
---------------------------------------------------------------------------
-- | Abstract bookmark view object.
--
data BookmarkView = BookmarkView {
bookmarkStore :: ListStore (Timestamp, Text)
}
-- | The actions to take in response to TraceView events.
--
data BookmarkViewActions = BookmarkViewActions {
bookmarkViewAddBookmark :: IO (),
bookmarkViewRemoveBookmark :: Int -> IO (),
bookmarkViewGotoBookmark :: Timestamp -> IO (),
bookmarkViewEditLabel :: Int -> Text -> IO ()
}
---------------------------------------------------------------------------
bookmarkViewAdd :: BookmarkView -> Timestamp -> Text -> IO ()
bookmarkViewAdd BookmarkView{bookmarkStore} ts label = do
listStoreAppend bookmarkStore (ts, label)
return ()
bookmarkViewRemove :: BookmarkView -> Int -> IO ()
bookmarkViewRemove BookmarkView{bookmarkStore} n = do
listStoreRemove bookmarkStore n
return ()
bookmarkViewClear :: BookmarkView -> IO ()
bookmarkViewClear BookmarkView{bookmarkStore} =
listStoreClear bookmarkStore
bookmarkViewGet :: BookmarkView -> IO [(Timestamp, Text)]
bookmarkViewGet BookmarkView{bookmarkStore} =
listStoreToList bookmarkStore
bookmarkViewSetLabel :: BookmarkView -> Int -> Text -> IO ()
bookmarkViewSetLabel BookmarkView{bookmarkStore} n label = do
(ts,_) <- listStoreGetValue bookmarkStore n
listStoreSetValue bookmarkStore n (ts, label)
---------------------------------------------------------------------------
bookmarkViewNew :: Builder -> BookmarkViewActions -> IO BookmarkView
bookmarkViewNew builder BookmarkViewActions{..} = do
let getWidget cast name = builderGetObject builder cast name
---------------------------------------------------------------------------
bookmarkTreeView <- getWidget castToTreeView "bookmark_list"
bookmarkStore <- listStoreNew []
columnTs <- treeViewColumnNew
cellTs <- cellRendererTextNew
columnLabel <- treeViewColumnNew
cellLabel <- cellRendererTextNew
selection <- treeViewGetSelection bookmarkTreeView
treeViewColumnSetTitle columnTs "Time"
treeViewColumnSetTitle columnLabel "Label"
treeViewColumnPackStart columnTs cellTs False
treeViewColumnPackStart columnLabel cellLabel True
treeViewAppendColumn bookmarkTreeView columnTs
treeViewAppendColumn bookmarkTreeView columnLabel
Compat.treeViewSetModel bookmarkTreeView (Just bookmarkStore)
cellLayoutSetAttributes columnTs cellTs bookmarkStore $ \(ts,_) ->
[ cellText := showFFloat (Just 6) (fromIntegral ts / 1000000) "s" ]
cellLayoutSetAttributes columnLabel cellLabel bookmarkStore $ \(_,label) ->
[ cellText := label ]
---------------------------------------------------------------------------
addBookmarkButton <- getWidget castToToolButton "add_bookmark_button"
deleteBookmarkButton <- getWidget castToToolButton "delete_bookmark"
gotoBookmarkButton <- getWidget castToToolButton "goto_bookmark_button"
onToolButtonClicked addBookmarkButton $
bookmarkViewAddBookmark
onToolButtonClicked deleteBookmarkButton $ do
selected <- treeSelectionGetSelected selection
case selected of
Nothing -> return ()
Just iter ->
let pos = listStoreIterToIndex iter
in bookmarkViewRemoveBookmark pos
onToolButtonClicked gotoBookmarkButton $ do
selected <- treeSelectionGetSelected selection
case selected of
Nothing -> return ()
Just iter -> do
let pos = listStoreIterToIndex iter
(ts,_) <- listStoreGetValue bookmarkStore pos
bookmarkViewGotoBookmark ts
bookmarkTreeView `on` rowActivated $ \[pos] _ -> do
(ts, _) <- listStoreGetValue bookmarkStore pos
bookmarkViewGotoBookmark ts
set cellLabel [ cellTextEditable := True ]
on cellLabel edited $ \[pos] val -> do
bookmarkViewEditLabel pos val
---------------------------------------------------------------------------
return BookmarkView{..}
================================================
FILE: GUI/ConcurrencyControl.hs
================================================
module GUI.ConcurrencyControl (
ConcurrencyControl,
start,
fullSpeed,
) where
import qualified System.Glib.MainLoop as Glib
import qualified Control.Concurrent as Concurrent
import qualified Control.Exception as Exception
import Control.Concurrent.MVar
newtype ConcurrencyControl = ConcurrencyControl (MVar (Int, Glib.HandlerId))
-- | Setup cooperative thread scheduling with Gtk+.
--
start :: IO ConcurrencyControl
start = do
handlerId <- normalScheduling
return . ConcurrencyControl =<< newMVar (0, handlerId)
-- | Run an expensive action that needs to use all the available CPU power.
--
-- The normal cooperative GUI thread scheduling does not work so well in this
-- case so we use an alternative technique. We can't use this one all the time
-- however or we'd hog the CPU even when idle.
--
fullSpeed :: ConcurrencyControl -> IO a -> IO a
fullSpeed (ConcurrencyControl handlerRef) =
Exception.bracket_ begin end
where
-- remove the normal scheduling handler and put in the full speed one
begin = do
(count, handlerId) <- takeMVar handlerRef
if count == 0
-- nobody else is running fullSpeed
then do Glib.timeoutRemove handlerId
handlerId' <- fullSpeedScheduling
putMVar handlerRef (1, handlerId')
-- we're already running fullSpeed, just inc the count
else do putMVar handlerRef (count+1, handlerId)
-- reinstate the normal scheduling
end = do
(count, handlerId) <- takeMVar handlerRef
if count == 1
-- just us running fullSpeed so we clean up
then do Glib.timeoutRemove handlerId
handlerId' <- normalScheduling
putMVar handlerRef (0, handlerId')
-- someone else running fullSpeed, they're responsible for stopping
else do putMVar handlerRef (count-1, handlerId)
normalScheduling :: IO Glib.HandlerId
normalScheduling =
Glib.timeoutAddFull
(Concurrent.yield >> return True)
Glib.priorityDefaultIdle 50
--50ms, ie 20 times a second.
fullSpeedScheduling :: IO Glib.HandlerId
fullSpeedScheduling =
Glib.idleAdd
(Concurrent.yield >> return True)
Glib.priorityDefaultIdle
================================================
FILE: GUI/DataFiles.hs
================================================
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module GUI.DataFiles
( ui
, loadLogo
) where
import Control.Exception (IOException, Handler(..), catches)
import System.IO
import Data.FileEmbed
import Graphics.UI.Gtk (Pixbuf, pixbufNewFromFile)
import Language.Haskell.TH
import System.Glib (GError)
import System.IO.Temp
import qualified Data.ByteString as B
import qualified Data.Text.Encoding as TE
uiFile :: FilePath
uiFile = "threadscope.ui"
logoFile :: FilePath
logoFile = "threadscope.png"
-- | Textual representation of the UI file
ui :: Q Exp
ui = [| TE.decodeUtf8 $(makeRelativeToProject uiFile >>= embedFile) |]
renderLogo :: B.ByteString -> IO (Maybe Pixbuf)
renderLogo bytes =
withSystemTempFile logoFile $ \path h -> do
B.hPut h bytes
hClose h
Just <$> pixbufNewFromFile path
`catches`
-- in case of a failure in the file IO or pixbufNewFromFile, return Nothing
[ Handler $ \(_ :: IOException) -> return Nothing
, Handler $ \(_ :: GError) -> return Nothing
]
-- | Load the logo file as a 'Pixbuf'.
loadLogo :: Q Exp
loadLogo = [| renderLogo $(makeRelativeToProject logoFile >>= embedFile) |]
================================================
FILE: GUI/Dialogs.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
module GUI.Dialogs where
import GUI.DataFiles (loadLogo)
import Paths_threadscope (version)
import Graphics.UI.Gtk
import Data.Version (showVersion)
import System.FilePath
import Control.Monad.Trans
-------------------------------------------------------------------------------
aboutDialog :: WindowClass window => window -> IO ()
aboutDialog parent
= do dialog <- aboutDialogNew
logo <- $loadLogo
set dialog [
aboutDialogName := "ThreadScope",
aboutDialogVersion := showVersion version,
aboutDialogCopyright := "Released under the GHC license as part of the Glasgow Haskell Compiler.",
aboutDialogComments := "A GHC eventlog profile viewer",
aboutDialogAuthors := ["Donnie Jones <donnie@darthik.com>",
"Simon Marlow <simonm@microsoft.com>",
"Satnam Singh <s.singh@ieee.org>",
"Duncan Coutts <duncan@well-typed.com>",
"Mikolaj Konarski <mikolaj@well-typed.com>",
"Nicolas Wu <nick@well-typed.com>",
"Eric Kow <eric@well-typed.com>"],
aboutDialogLogo := logo,
aboutDialogWebsite := "http://www.haskell.org/haskellwiki/ThreadScope",
windowTransientFor := toWindow parent
]
dialog `on` response $ \_ -> widgetDestroy dialog
widgetShow dialog
-------------------------------------------------------------------------------
openFileDialog :: WindowClass window => window -> (FilePath -> IO ()) -> IO ()
openFileDialog parent open
= do dialog <- fileChooserDialogNew
(Just "Open Profile...")
(Just (toWindow parent))
FileChooserActionOpen
[("gtk-cancel", ResponseCancel)
,("gtk-open", ResponseAccept)]
set dialog [
windowModal := True
]
eventlogfiles <- fileFilterNew
fileFilterSetName eventlogfiles "GHC eventlog files (*.eventlog)"
fileFilterAddPattern eventlogfiles "*.eventlog"
fileChooserAddFilter dialog eventlogfiles
allfiles <- fileFilterNew
fileFilterSetName allfiles "All files"
fileFilterAddPattern allfiles "*"
fileChooserAddFilter dialog allfiles
dialog `on` response $ \response -> do
case response of
ResponseAccept -> do
mfile <- fileChooserGetFilename dialog
case mfile of
Just file -> open file
Nothing -> return ()
_ -> return ()
widgetDestroy dialog
widgetShowAll dialog
-------------------------------------------------------------------------------
data FileExportFormat = FormatPDF | FormatPNG
exportFileDialog :: WindowClass window => window
-> FilePath
-> (FilePath -> FileExportFormat -> IO ())
-> IO ()
exportFileDialog parent oldfile save = do
dialog <- fileChooserDialogNew
(Just "Save timeline image...")
(Just (toWindow parent))
FileChooserActionSave
[("gtk-cancel", ResponseCancel)
,("gtk-save", ResponseAccept)]
set dialog [
fileChooserDoOverwriteConfirmation := True,
windowModal := True
]
let (olddir, oldfilename) = splitFileName oldfile
fileChooserSetCurrentName dialog (replaceExtension oldfilename "png")
fileChooserSetCurrentFolder dialog olddir
pngFiles <- fileFilterNew
fileFilterSetName pngFiles "PNG bitmap files"
fileFilterAddPattern pngFiles "*.png"
fileChooserAddFilter dialog pngFiles
pdfFiles <- fileFilterNew
fileFilterSetName pdfFiles "PDF files"
fileFilterAddPattern pdfFiles "*.pdf"
fileChooserAddFilter dialog pdfFiles
dialog `on` response $ \response ->
case response of
ResponseAccept -> do
mfile <- fileChooserGetFilename dialog
case mfile of
Just file
| takeExtension file == ".pdf" -> do
save file FormatPDF
widgetDestroy dialog
| takeExtension file == ".png" -> do
save file FormatPNG
widgetDestroy dialog
| otherwise ->
formatError dialog
Nothing -> widgetDestroy dialog
_ -> widgetDestroy dialog
widgetShowAll dialog
where
formatError dialog = do
msg <- messageDialogNew (Just (toWindow dialog))
[DialogModal, DialogDestroyWithParent]
MessageError ButtonsClose
"The file format is unknown or unsupported"
set msg [
messageDialogSecondaryText := Just $
"The PNG and PDF formats are supported. "
++ "Please use a file extension of '.png' or '.pdf'."
]
dialogRun msg
widgetDestroy msg
-------------------------------------------------------------------------------
errorMessageDialog :: WindowClass window => window -> String -> String -> IO ()
errorMessageDialog parent headline explanation = do
dialog <- messageDialogNew (Just (toWindow parent))
[] MessageError ButtonsNone ""
set dialog
[ windowModal := True
, windowTransientFor := toWindow parent
, messageDialogText := Just headline
, messageDialogSecondaryText := Just explanation
, windowResizable := True
]
dialogAddButton dialog "Close" ResponseClose
dialogSetDefaultResponse dialog ResponseClose
dialog `on` response $ \_-> widgetDestroy dialog
widgetShowAll dialog
================================================
FILE: GUI/EventsView.hs
================================================
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module GUI.EventsView (
EventsView,
eventsViewNew,
EventsViewActions(..),
eventsViewSetEvents,
eventsViewGetCursor,
eventsViewSetCursor,
eventsViewScrollToLine,
) where
import GHC.RTS.Events
import Graphics.UI.Gtk hiding (rectangle)
import Graphics.Rendering.Cairo
import GUI.ViewerColours
import Control.Monad
import Data.Array
import Data.Monoid
import Data.IORef
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.Text.Lazy.Builder.Int as TB (decimal)
import Numeric
import Prelude
-------------------------------------------------------------------------------
data EventsView = EventsView {
drawArea :: !Widget,
adj :: !Adjustment,
stateRef :: !(IORef ViewState)
}
data EventsViewActions = EventsViewActions {
eventsViewCursorChanged :: Int -> IO ()
}
data ViewState = ViewState {
lineHeight :: !Double,
eventsState :: !EventsState
}
data EventsState
= EventsEmpty
| EventsLoaded {
cursorPos :: !Int,
mrange :: !(Maybe (Int, Int)),
eventsArr :: Array Int Event
}
-------------------------------------------------------------------------------
eventsViewNew :: Builder -> EventsViewActions -> IO EventsView
eventsViewNew builder EventsViewActions{..} = do
stateRef <- newIORef undefined
let getWidget cast = builderGetObject builder cast
drawArea <- getWidget castToWidget ("eventsDrawingArea" :: T.Text)
vScrollbar <- getWidget castToVScrollbar ("eventsVScroll" :: T.Text)
adj <- get vScrollbar rangeAdjustment
widgetSetCanFocus drawArea True
--TODO: needs to be reset on each style change ^^
-----------------------------------------------------------------------------
-- Line height
-- Calculate the height of each line based on the current font
let getLineHeight = do
pangoCtx <- widgetGetPangoContext drawArea
fontDesc <- contextGetFontDescription pangoCtx
metrics <- contextGetMetrics pangoCtx fontDesc emptyLanguage
return $ ascent metrics + descent metrics --TODO: padding?
-- We cache the height of each line
initialLineHeight <- getLineHeight
-- but have to update it when the font changes
on drawArea styleSet $ \_ -> do
lineHeight' <- getLineHeight
modifyIORef stateRef $ \viewstate -> viewstate { lineHeight = lineHeight' }
-----------------------------------------------------------------------------
writeIORef stateRef ViewState {
lineHeight = initialLineHeight,
eventsState = EventsEmpty
}
let eventsView = EventsView {..}
-----------------------------------------------------------------------------
-- Drawing
on drawArea draw $ liftIO $ do
drawEvents eventsView =<< readIORef stateRef
return ()
-----------------------------------------------------------------------------
-- Key navigation
on drawArea keyPressEvent $ do
let scroll by = liftIO $ do
ViewState{eventsState, lineHeight} <- readIORef stateRef
pagesize <- get adj adjustmentPageSize
let pagejump = max 1 (truncate (pagesize / lineHeight) - 1)
case eventsState of
EventsEmpty -> return ()
EventsLoaded{cursorPos, eventsArr} ->
eventsViewCursorChanged cursorPos'
where
cursorPos' = clampBounds range (by pagejump end cursorPos)
range@(_,end) = bounds eventsArr
return True
key <- eventKeyName
#if MIN_VERSION_gtk3(0,13,0)
case T.unpack key of
#else
case key of
#endif
"Up" -> scroll (\_page _end pos -> pos-1)
"Down" -> scroll (\_page _end pos -> pos+1)
"Page_Up" -> scroll (\ page _end pos -> pos-page)
"Page_Down" -> scroll (\ page _end pos -> pos+page)
"Home" -> scroll (\_page _end _pos -> 0)
"End" -> scroll (\_page end _pos -> end)
"Left" -> return True
"Right" -> return True
_ -> return False
-----------------------------------------------------------------------------
-- Scrolling
set adj [ adjustmentLower := 0 ]
on drawArea sizeAllocate $ \_ ->
updateScrollAdjustment eventsView =<< readIORef stateRef
let hitpointToLine :: ViewState -> Double -> Double -> Maybe Int
hitpointToLine ViewState{eventsState = EventsEmpty} _ _ = Nothing
hitpointToLine ViewState{eventsState = EventsLoaded{eventsArr}, lineHeight}
yOffset eventY
| hitLine > maxIndex = Nothing
| otherwise = Just hitLine
where
hitLine = truncate ((yOffset + eventY) / lineHeight)
maxIndex = snd (bounds eventsArr)
on drawArea buttonPressEvent $ tryEvent $ do
(_,y) <- eventCoordinates
liftIO $ do
viewState <- readIORef stateRef
yOffset <- get adj adjustmentValue
widgetGrabFocus drawArea
case hitpointToLine viewState yOffset y of
Nothing -> return ()
Just n -> eventsViewCursorChanged n
on drawArea scrollEvent $ do
dir <- eventScrollDirection
liftIO $ do
val <- get adj adjustmentValue
upper <- get adj adjustmentUpper
pagesize <- get adj adjustmentPageSize
step <- get adj adjustmentStepIncrement
case dir of
ScrollUp -> set adj [ adjustmentValue := val - step ]
ScrollDown -> set adj [ adjustmentValue := min (val + step)
(upper - pagesize) ]
_ -> return ()
return True
onValueChanged adj $
widgetQueueDraw drawArea
-----------------------------------------------------------------------------
return eventsView
-------------------------------------------------------------------------------
eventsViewSetEvents :: EventsView -> Maybe (Array Int Event) -> IO ()
eventsViewSetEvents eventWin@EventsView{drawArea, stateRef} mevents = do
viewState <- readIORef stateRef
let eventsState' = case mevents of
Nothing -> EventsEmpty
Just events -> EventsLoaded {
cursorPos = 0,
mrange = Nothing,
eventsArr = events
}
viewState' = viewState { eventsState = eventsState' }
writeIORef stateRef viewState'
updateScrollAdjustment eventWin viewState'
widgetQueueDraw drawArea
-------------------------------------------------------------------------------
eventsViewGetCursor :: EventsView -> IO (Maybe Int)
eventsViewGetCursor EventsView{stateRef} = do
ViewState{eventsState} <- readIORef stateRef
case eventsState of
EventsEmpty -> return Nothing
EventsLoaded{cursorPos} -> return (Just cursorPos)
eventsViewSetCursor :: EventsView -> Int -> Maybe (Int, Int) -> IO ()
eventsViewSetCursor eventsView@EventsView{drawArea, stateRef} n mrange = do
viewState@ViewState{eventsState} <- readIORef stateRef
case eventsState of
EventsEmpty -> return ()
EventsLoaded{eventsArr} -> do
let n' = clampBounds (bounds eventsArr) n
writeIORef stateRef viewState {
eventsState = eventsState { cursorPos = n', mrange }
}
eventsViewScrollToLine eventsView n'
widgetQueueDraw drawArea
eventsViewScrollToLine :: EventsView -> Int -> IO ()
eventsViewScrollToLine EventsView{adj, stateRef} n = do
ViewState{lineHeight} <- readIORef stateRef
-- make sure that the range [n..n+1] is within the current page:
adjustmentClampPage adj
(fromIntegral n * lineHeight)
(fromIntegral (n+1) * lineHeight)
-------------------------------------------------------------------------------
updateScrollAdjustment :: EventsView -> ViewState -> IO ()
updateScrollAdjustment EventsView{drawArea, adj}
ViewState{lineHeight, eventsState} = do
Rectangle _ _ _ windowHeight <- widgetGetAllocation drawArea
let numLines = case eventsState of
EventsEmpty -> 0
EventsLoaded{eventsArr} -> snd (bounds eventsArr) + 1
linesHeight = fromIntegral numLines * lineHeight
upper = max linesHeight (fromIntegral windowHeight)
pagesize = fromIntegral windowHeight
set adj [
adjustmentUpper := upper,
adjustmentPageSize := pagesize,
adjustmentStepIncrement := pagesize * 0.2,
adjustmentPageIncrement := pagesize * 0.9
]
val <- get adj adjustmentValue
when (val > upper - pagesize) $
set adj [ adjustmentValue := max 0 (upper - pagesize) ]
-------------------------------------------------------------------------------
drawEvents :: EventsView -> ViewState -> IO ()
drawEvents _ ViewState {eventsState = EventsEmpty} = return ()
drawEvents EventsView{drawArea, adj}
ViewState {lineHeight, eventsState = EventsLoaded{..}} = do
yOffset <- get adj adjustmentValue
pageSize <- get adj adjustmentPageSize
-- calculate which lines are visible
let lower = truncate (yOffset / lineHeight)
upper = ceiling ((yOffset + pageSize) / lineHeight)
-- the array indexes [begin..end] inclusive
-- are partially or fully visible
begin = lower
end = min upper (snd (bounds eventsArr))
-- TODO: don't use Just here
Just win <- widgetGetWindow drawArea
style <- widgetGetStyle drawArea
focused <- widgetGetIsFocus drawArea
let state | focused = StateSelected
| otherwise = StateActive
pangoCtx <- widgetGetPangoContext drawArea
layout <- layoutEmpty pangoCtx
layoutSetEllipsize layout EllipsizeEnd
(Rectangle _ _ width _) <- widgetGetAllocation drawArea
let clipRect = Rectangle 0 0 0 0
let -- With average char width, timeWidth is enough for 24 hours of logs
-- (way more than TS can handle, currently). Aligns nicely with
-- current timeline_yscale_area width, too.
-- TODO: take timeWidth from the yScaleDrawingArea width
-- TODO: perhaps make the timeWidth area grey, too?
-- TODO: perhaps limit scroll to the selected interval (perhaps not strictly, but only so that the interval area does not completely vanish from the visible area)?
timeWidth = 105
columnGap = 20
descrWidth = width - timeWidth - columnGap
sequence_
[ do when (inside || selected) $
renderWithDrawWindow win $ do
setSourceRGBAForStyle styleGetBackground style state1
rectangle 0 y (fromIntegral width) lineHeight
fill
-- The event time
layoutSetText layout (showEventTime event)
layoutSetAlignment layout AlignRight
layoutSetWidth layout (Just (fromIntegral timeWidth))
renderWithDrawWindow win $ do
setForegroundColor style state2
moveTo 0 y
showLayout layout
-- The event description text
layoutSetText layout (showEventDescr event)
layoutSetAlignment layout AlignLeft
layoutSetWidth layout (Just (fromIntegral descrWidth))
renderWithDrawWindow win $ do
setForegroundColor style state2
moveTo (fromIntegral $ timeWidth + columnGap) y
showLayout layout
| n <- [begin..end]
, let y = fromIntegral n * lineHeight - yOffset
event = eventsArr ! n
inside = maybe False (\ (s, e) -> s <= n && n <= e) mrange
selected = cursorPos == n
(state1, state2)
| inside = (StateSelected, StateSelected)
| selected = (StateSelected, state)
| otherwise = (state, StateNormal)
]
where
showEventTime (Event time _spec _) =
showFFloat (Just 6) (fromIntegral time / 1000000) "s"
showEventDescr :: Event -> T.Text
showEventDescr (Event _time spec cap) = TL.toStrict $ TB.toLazyText $
maybe "" (\c -> "HEC " <> TB.decimal c <> ": ") cap
<> case spec of
UnknownEvent{ref} -> "unknown event; " <> TB.decimal ref
Message msg -> TB.fromText msg
UserMessage msg -> TB.fromText msg
_ -> buildEventInfo spec
setForegroundColor = setSourceRGBAForStyle styleGetForeground
-------------------------------------------------------------------------------
clampBounds :: Ord a => (a, a) -> a -> a
clampBounds (lower, upper) x
| x <= lower = lower
| x > upper = upper
| otherwise = x
================================================
FILE: GUI/GtkExtras.hs
================================================
{-# LANGUAGE ForeignFunctionInterface, CPP #-}
module GUI.GtkExtras where
-- This is all stuff that should be bound in the gtk package but is not yet
-- (as of gtk-0.12.0)
import Graphics.UI.GtkInternals
import Graphics.UI.Gtk (Rectangle)
import System.Glib.MainLoop
import Graphics.Rendering.Pango.Types
import Graphics.Rendering.Pango.BasicTypes
import Graphics.UI.Gtk.General.Enums (StateType, ShadowType)
import Foreign
import Foreign.C
import Control.Concurrent.MVar
#if mingw32_HOST_OS || mingw32_TARGET_OS
#include "windows_cconv.h"
#else
import System.Glib.GError
import Control.Monad
#endif
waitGUI :: IO ()
waitGUI = do
resultVar <- newEmptyMVar
idleAdd (putMVar resultVar () >> return False) priorityDefaultIdle
takeMVar resultVar
-------------------------------------------------------------------------------
launchProgramForURI :: String -> IO Bool
#if mingw32_HOST_OS || mingw32_TARGET_OS
launchProgramForURI uri = do
withCString "open" $ \verbPtr ->
withCString uri $ \filePtr ->
c_ShellExecuteA
nullPtr
verbPtr
filePtr
nullPtr
nullPtr
1 -- SW_SHOWNORMAL
return True
foreign import WINDOWS_CCONV unsafe "shlobj.h ShellExecuteA"
c_ShellExecuteA :: Ptr () -- HWND hwnd
-> CString -- LPCTSTR lpOperation
-> CString -- LPCTSTR lpFile
-> CString -- LPCTSTR lpParameters
-> CString -- LPCTSTR lpDirectory
-> CInt -- INT nShowCmd
-> IO CInt -- HINSTANCE return
#else
launchProgramForURI uri =
propagateGError $ \errPtrPtr ->
withCString uri $ \uriStrPtr -> do
timestamp <- gtk_get_current_event_time
liftM toBool $ gtk_show_uri nullPtr uriStrPtr timestamp errPtrPtr
#endif
-------------------------------------------------------------------------------
foreign import ccall safe "gtk_show_uri"
gtk_show_uri :: Ptr Screen -> Ptr CChar -> CUInt -> Ptr (Ptr ()) -> IO CInt
foreign import ccall unsafe "gtk_get_current_event_time"
gtk_get_current_event_time :: IO CUInt
================================================
FILE: GUI/Histogram.hs
================================================
{-# LANGUAGE ScopedTypeVariables #-}
module GUI.Histogram (
HistogramView,
histogramViewNew,
histogramViewSetHECs,
histogramViewSetInterval,
) where
import Events.HECs
import GUI.Timeline.Render (renderTraces, renderYScaleArea)
import GUI.Timeline.Render.Constants
import GUI.Types
import qualified Graphics.Rendering.Cairo as C
import Graphics.UI.Gtk
import qualified GUI.GtkExtras as GtkExt
import Data.IORef
import Control.Monad.Trans
data HistogramView =
HistogramView
{ hecsIORef :: IORef (Maybe HECs)
, mintervalIORef :: IORef (Maybe Interval)
, histogramDrawingArea :: DrawingArea
, histogramYScaleArea :: DrawingArea
}
histogramViewSetHECs :: HistogramView -> Maybe HECs -> IO ()
histogramViewSetHECs HistogramView{..} mhecs = do
writeIORef hecsIORef mhecs
writeIORef mintervalIORef Nothing -- the old interval may make no sense
widgetQueueDraw histogramDrawingArea
widgetQueueDraw histogramYScaleArea
histogramViewSetInterval :: HistogramView -> Maybe Interval -> IO ()
histogramViewSetInterval HistogramView{..} minterval = do
writeIORef mintervalIORef minterval
widgetQueueDraw histogramDrawingArea
widgetQueueDraw histogramYScaleArea
histogramViewNew :: Builder -> IO HistogramView
histogramViewNew builder = do
let getWidget cast = builderGetObject builder cast
histogramDrawingArea <- getWidget castToDrawingArea "histogram_drawingarea"
histogramYScaleArea <- getWidget castToDrawingArea "timeline_yscale_area2"
timelineXScaleArea <- getWidget castToDrawingArea "timeline_xscale_area"
-- HACK: layoutSetAttributes does not work for \mu, so let's work around
fd <- fontDescriptionNew
fontDescriptionSetSize fd 8
fontDescriptionSetFamily fd "sans serif"
widgetModifyFont histogramYScaleArea (Just fd)
Rectangle _ _ _ xh <- widgetGetAllocation timelineXScaleArea
let xScaleAreaHeight = fromIntegral xh
traces = [TraceHistogram]
paramsHist (w, h) minterval = ViewParameters
{ width = w
, height = h
, viewTraces = traces
, hadjValue = 0
, scaleValue = 1
, maxSpkValue = undefined
, detail = undefined
, bwMode = undefined
, labelsMode = False
, histogramHeight = h - histXScaleHeight
, minterval = minterval
, xScaleAreaHeight = xScaleAreaHeight
}
hecsIORef <- newIORef Nothing
mintervalIORef <- newIORef Nothing
pangoCtx <- widgetGetPangoContext histogramDrawingArea
style <- get histogramDrawingArea widgetStyle
layout <- layoutEmpty pangoCtx
(_ :: String) <- layoutSetMarkup layout $
"No detailed spark events in this eventlog.\n"
++ "Re-run with <tt>+RTS -lf</tt> to generate them."
-- Program the callback for the capability drawingArea
on histogramDrawingArea draw $
C.liftIO $ do
maybeEventArray <- readIORef hecsIORef
-- TODO: get rid of Just
Just win <- widgetGetWindow histogramDrawingArea
Rectangle _ _ w windowHeight <- widgetGetAllocation histogramDrawingArea
case maybeEventArray of
Nothing -> return ()
Just hecs
| null (durHistogram hecs) -> do
renderWithDrawWindow win $ do
C.moveTo 4 20
showLayout layout
return ()
| otherwise -> do
minterval <- readIORef mintervalIORef
if windowHeight < 80
then return ()
else do
let size = (w, windowHeight - firstTraceY)
params = paramsHist size minterval
rect = Rectangle 0 0 w (snd size)
renderWithDrawWindow win $
renderTraces params hecs rect
return ()
-- Redrawing histogramYScaleArea
histogramYScaleArea `on` draw $ liftIO $ do
maybeEventArray <- readIORef hecsIORef
case maybeEventArray of
Nothing -> return ()
Just hecs
| null (durHistogram hecs) -> return ()
| otherwise -> do
-- TODO: get rid of Just
Just win <- widgetGetWindow histogramYScaleArea
minterval <- readIORef mintervalIORef
Rectangle _ _ _ windowHeight <- widgetGetAllocation histogramYScaleArea
if windowHeight < 80
then return ()
else do
let size = (undefined, windowHeight - firstTraceY)
params = paramsHist size minterval
renderWithDrawWindow win $
renderYScaleArea params hecs histogramYScaleArea
return ()
return HistogramView{..}
================================================
FILE: GUI/KeyView.hs
================================================
module GUI.KeyView (
KeyView,
keyViewNew,
) where
import GUI.ViewerColours
import GUI.Timeline.Render.Constants
import Graphics.UI.Gtk
import qualified Graphics.UI.Gtk.ModelView.TreeView.Compat as Compat
import qualified Graphics.Rendering.Cairo as C
---------------------------------------------------------------------------
-- | Abstract key view object.
--
data KeyView = KeyView
---------------------------------------------------------------------------
keyViewNew :: Builder -> IO KeyView
keyViewNew builder = do
keyTreeView <- builderGetObject builder castToTreeView "key_list"
-- TODO: get rid of this Just
Just dw <- widgetGetWindow keyTreeView
keyEntries <- createKeyEntries dw keyData
keyStore <- listStoreNew keyEntries
keyColumn <- treeViewColumnNew
imageCell <- cellRendererPixbufNew
labelCell <- cellRendererTextNew
treeViewColumnPackStart keyColumn imageCell False
treeViewColumnPackStart keyColumn labelCell True
treeViewAppendColumn keyTreeView keyColumn
selection <- treeViewGetSelection keyTreeView
treeSelectionSetMode selection SelectionNone
let tooltipColumn = makeColumnIdString 0
customStoreSetColumn keyStore tooltipColumn (\(_,tooltip,_) -> tooltip)
Compat.treeViewSetModel keyTreeView (Just keyStore)
set keyTreeView [ treeViewTooltipColumn := tooltipColumn ]
cellLayoutSetAttributes keyColumn imageCell keyStore $ \(_,_,img) ->
[ cellPixbuf := img ]
cellLayoutSetAttributes keyColumn labelCell keyStore $ \(label,_,_) ->
[ cellText := label ]
---------------------------------------------------------------------------
return KeyView
-------------------------------------------------------------------------------
data KeyStyle = KDuration | KEvent | KEventAndGraph
keyData :: [(String, KeyStyle, Color, String)]
keyData =
[ ("running", KDuration, runningColour,
"Indicates a period of time spent running Haskell code (not GC, not blocked/idle)")
, ("GC", KDuration, gcColour,
"Indicates a period of time spent by the RTS performing garbage collection (GC)")
, ("GC waiting", KDuration, gcWaitColour,
"Indicates a period of time spent by the RTS waiting to initiate or finish garbage collection (GC)")
, ("create thread", KEvent, createThreadColour,
"Indicates a new Haskell thread has been created")
, ("seq GC req", KEvent, seqGCReqColour,
"Indicates a HEC has requested to start a sequential GC")
, ("par GC req", KEvent, parGCReqColour,
"Indicates a HEC has requested to start a parallel GC")
, ("migrate thread", KEvent, migrateThreadColour,
"Indicates a Haskell thread has been moved from one HEC to another")
, ("thread wakeup", KEvent, threadWakeupColour,
"Indicates that a thread that was previously blocked (e.g. I/O, MVar etc) is now ready to run")
, ("shutdown", KEvent, shutdownColour,
"Indicates a HEC is terminating")
, ("user message", KEvent, userMessageColour,
"Indicates a message generated from Haskell code (via traceEvent)")
, ("perf counter", KEvent, createdConvertedColour,
"Indicates an update of a perf counter")
, ("perf tracepoint", KEvent, shutdownColour,
"Indicates that a perf tracepoint was reached")
, ("create spark", KEventAndGraph, createdConvertedColour,
"As an event it indicates a use of `par` resulted in a spark being " ++
"created (and added to the spark pool). In the spark creation " ++
"graph the coloured area represents the number of sparks created.")
, ("dud spark", KEventAndGraph, fizzledDudsColour,
"As an event it indicates a use of `par` resulted in the spark being " ++
"discarded because it was a 'dud' (already evaluated). In the spark " ++
"creation graph the coloured area represents the number of dud sparks.")
, ("overflowed spark",KEventAndGraph, overflowedColour,
"As an event it indicates a use of `par` resulted in the spark being " ++
"discarded because the spark pool was full. In the spark creation " ++
"graph the coloured area represents the number of overflowed sparks.")
, ("run spark", KEventAndGraph, createdConvertedColour,
"As an event it indicates a spark has started to be run/evaluated. " ++
"In the spark conversion graph the coloured area represents the number " ++
"of sparks run.")
, ("fizzled spark", KEventAndGraph, fizzledDudsColour,
"As an event it indicates a spark has 'fizzled', meaning it has been " ++
"discovered that the spark's thunk was evaluated by some other thread. " ++
"In the spark conversion graph the coloured area represents the number " ++
"of sparks that have fizzled.")
, ("GCed spark", KEventAndGraph, gcColour,
"As an event it indicates a spark has been GCed, meaning it has been " ++
"discovered that the spark's thunk was no longer needed anywhere. " ++
"In the spark conversion graph the coloured area represents the number " ++
"of sparks that were GCed.")
]
createKeyEntries :: DrawWindowClass dw
=> dw
-> [(String, KeyStyle, Color,String)]
-> IO [(String, String, Pixbuf)]
createKeyEntries similar entries =
sequence
[ do pixbuf <- renderToPixbuf similar (50, hecBarHeight) $ do
C.setSourceRGB 1 1 1
C.paint
renderKeyIcon style colour
return (label, tooltip, pixbuf)
| (label, style, colour, tooltip) <- entries ]
renderKeyIcon :: KeyStyle -> Color -> C.Render ()
renderKeyIcon KDuration keyColour = do
setSourceRGBAhex keyColour 1.0
let x = fromIntegral ox
C.rectangle (x - 2) 5 38 (fromIntegral (hecBarHeight `div` 2))
C.fill
renderKeyIcon KEvent keyColour = renderKEvent keyColour
renderKeyIcon KEventAndGraph keyColour = do
renderKEvent keyColour
-- An icon roughly representing a jaggedy graph.
let x = fromIntegral ox
y = fromIntegral hecBarHeight
C.moveTo (2*x) (y - 2)
C.relLineTo 3 (-6)
C.relLineTo 3 0
C.relLineTo 3 3
C.relLineTo 5 1
C.relLineTo 1 (-(y - 4))
C.relLineTo 2 (y - 4)
C.relLineTo 1 (-(y - 4))
C.relLineTo 2 (y - 4)
C.lineTo (2*x+20) (y - 2)
C.fill
setSourceRGBAhex black 1.0
C.setLineWidth 1.0
C.moveTo (2*x-4) (y - 2.5)
C.lineTo (2*x+24) (y - 2.5)
C.stroke
renderKEvent :: Color -> C.Render ()
renderKEvent keyColour = do
setSourceRGBAhex keyColour 1.0
C.setLineWidth 3.0
let x = fromIntegral ox
C.moveTo x 0
C.relLineTo 0 25
C.stroke
renderToPixbuf :: DrawWindowClass dw => dw -> (Int, Int) -> C.Render ()
-> IO Pixbuf
renderToPixbuf similar (w, h) draw = do
renderWithDrawWindow similar draw
pixbuf <- pixbufNewFromWindow similar 0 0 w h
return pixbuf
-------------------------------------------------------------------------------
================================================
FILE: GUI/Main.hs
================================================
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module GUI.Main (runGUI) where
-- Imports for GTK
import qualified Graphics.UI.Gtk as Gtk
import System.Glib.GError (failOnGError)
-- Imports from Haskell library
import Text.Printf
#ifndef mingw32_HOST_OS
import System.Posix
#endif
import Control.Concurrent
import qualified Control.Concurrent.Chan as Chan
import Control.Exception
import Data.Array
import Data.Maybe
import Data.Text (Text)
-- Imports for ThreadScope
import qualified GUI.App as App
import qualified GUI.MainWindow as MainWindow
import GUI.Types
import Events.HECs hiding (Event)
import GUI.DataFiles (ui)
import GUI.Dialogs
import Events.ReadEvents
import GUI.EventsView
import GUI.SummaryView
import GUI.StartupInfoView
import GUI.Histogram
import GUI.Timeline
import GUI.TraceView
import GUI.BookmarkView
import GUI.KeyView
import GUI.SaveAs
import qualified GUI.ConcurrencyControl as ConcurrencyControl
import qualified GUI.ProgressView as ProgressView
import qualified GUI.GtkExtras as GtkExtras
-------------------------------------------------------------------------------
data UIEnv = UIEnv {
mainWin :: MainWindow.MainWindow,
eventsView :: EventsView,
startupView :: StartupInfoView,
summaryView :: SummaryView,
histogramView :: HistogramView,
timelineWin :: TimelineView,
traceView :: TraceView,
bookmarkView :: BookmarkView,
keyView :: KeyView,
eventQueue :: Chan Event,
concCtl :: ConcurrencyControl.ConcurrencyControl
}
data EventlogState
= NoEventlogLoaded
| EventlogLoaded {
mfilename :: Maybe FilePath, --test traces have no filepath
hecs :: HECs,
selection :: TimeSelection,
cursorPos :: Int
}
postEvent :: Chan Event -> Event -> IO ()
postEvent = Chan.writeChan
getEvent :: Chan Event -> IO Event
getEvent = Chan.readChan
data Event
= EventOpenDialog
| EventExportDialog
| EventLaunchWebsite
| EventLaunchTutorial
| EventAboutDialog
| EventQuit
| EventFileLoad FilePath
| EventTestLoad String
| EventFileReload
| EventFileExport FilePath FileExportFormat
| EventSetState HECs (Maybe FilePath) String Int Double
| EventShowSidebar Bool
| EventShowEvents Bool
| EventTimelineJumpStart
| EventTimelineJumpEnd
| EventTimelineJumpCursor
| EventTimelineScrollLeft
| EventTimelineScrollRight
| EventTimelineZoomIn
| EventTimelineZoomOut
| EventTimelineZoomToFit
| EventTimelineLabelsMode Bool
| EventTimelineShowBW Bool
| EventCursorChangedIndex Int
| EventCursorChangedSelection TimeSelection
| EventTracesChanged [Trace]
| EventBookmarkAdd
| EventBookmarkRemove Int
| EventBookmarkEdit Int Text
| EventUserError String SomeException
-- can add more specific ones if necessary
constructUI :: IO UIEnv
constructUI = failOnGError $ do
builder <- Gtk.builderNew
Gtk.builderAddFromString builder $ui
eventQueue <- Chan.newChan
let post = postEvent eventQueue
mainWin <- MainWindow.mainWindowNew builder MainWindow.MainWindowActions {
mainWinOpen = post EventOpenDialog,
mainWinExport = post EventExportDialog,
mainWinQuit = post EventQuit,
mainWinViewSidebar = post . EventShowSidebar,
mainWinViewEvents = post . EventShowEvents,
mainWinViewReload = post EventFileReload,
mainWinWebsite = post EventLaunchWebsite,
mainWinTutorial = post EventLaunchTutorial,
mainWinAbout = post EventAboutDialog,
mainWinJumpStart = post EventTimelineJumpStart,
mainWinJumpEnd = post EventTimelineJumpEnd,
mainWinJumpCursor = post EventTimelineJumpCursor,
mainWinScrollLeft = post EventTimelineScrollLeft,
mainWinScrollRight = post EventTimelineScrollRight,
mainWinJumpZoomIn = post EventTimelineZoomIn,
mainWinJumpZoomOut = post EventTimelineZoomOut,
mainWinJumpZoomFit = post EventTimelineZoomToFit,
mainWinDisplayLabels = post . EventTimelineLabelsMode,
mainWinViewBW = post . EventTimelineShowBW
}
timelineWin <- timelineViewNew builder TimelineViewActions {
timelineViewSelectionChanged = post . EventCursorChangedSelection
}
eventsView <- eventsViewNew builder EventsViewActions {
eventsViewCursorChanged = post . EventCursorChangedIndex
}
startupView <- startupInfoViewNew builder
summaryView <- summaryViewNew builder
histogramView <- histogramViewNew builder
traceView <- traceViewNew builder TraceViewActions {
traceViewTracesChanged = post . EventTracesChanged
}
bookmarkView <- bookmarkViewNew builder BookmarkViewActions {
bookmarkViewAddBookmark = post EventBookmarkAdd,
bookmarkViewRemoveBookmark = post . EventBookmarkRemove,
bookmarkViewGotoBookmark = \ts -> do
post (EventCursorChangedSelection (PointSelection ts))
post EventTimelineJumpCursor,
bookmarkViewEditLabel = \n v -> post (EventBookmarkEdit n v)
}
keyView <- keyViewNew builder
concCtl <- ConcurrencyControl.start
return UIEnv{..}
-------------------------------------------------------------------------------
data LoopDone = LoopDone
eventLoop :: UIEnv -> EventlogState -> IO ()
eventLoop uienv@UIEnv{..} eventlogState = do
event <- getEvent eventQueue
next <- dispatch event eventlogState
#if __GLASGOW_HASKELL__ <= 612
-- workaround for a wierd exception handling bug in ghc-6.12
`catch` \e -> throwIO (e :: SomeException)
#endif
case next of
Left LoopDone -> return ()
Right eventlogState' -> eventLoop uienv eventlogState'
where
dispatch :: Event -> EventlogState -> IO (Either LoopDone EventlogState)
dispatch EventQuit _ = return (Left LoopDone)
dispatch EventOpenDialog _ = do
openFileDialog mainWin $ \filename ->
post (EventFileLoad filename)
continue
dispatch (EventFileLoad filename) _ = do
async "loading the eventlog" $
loadEvents (Just filename) (registerEventsFromFile filename)
--TODO: set state to be empty during loading
continue
dispatch (EventTestLoad testname) _ = do
async "loading the test eventlog" $
loadEvents Nothing (registerEventsFromTrace testname)
--TODO: set state to be empty during loading
continue
dispatch EventFileReload EventlogLoaded{mfilename = Just filename} = do
async "reloading the eventlog" $
loadEvents (Just filename) (registerEventsFromFile filename)
--TODO: set state to be empty during loading
continue
dispatch EventFileReload EventlogLoaded{mfilename = Nothing} =
continue
-- dispatch EventClearState _
dispatch (EventSetState hecs mfilename name nevents timespan) _ =
-- We have to draw this ASAP, before the user manages to move
-- the mouse away from the window, or the window is left
-- in a partially drawn state.
ConcurrencyControl.fullSpeed concCtl $ do
MainWindow.setFileLoaded mainWin (Just name)
MainWindow.setStatusMessage mainWin $
printf "%s (%d events, %.3fs)" name nevents timespan
let mevents = Just $ hecEventArray hecs
eventsViewSetEvents eventsView mevents
startupInfoViewSetEvents startupView mevents
summaryViewSetEvents summaryView mevents
histogramViewSetHECs histogramView (Just hecs)
traceViewSetHECs traceView hecs
traces' <- traceViewGetTraces traceView
timelineWindowSetHECs timelineWin (Just hecs)
timelineWindowSetTraces timelineWin traces'
-- We set user 'traceMarker' events as initial bookmarks.
let usrMarkers = extractUserMarkers hecs
bookmarkViewClear bookmarkView
sequence_ [ bookmarkViewAdd bookmarkView ts label
| (ts, label) <- usrMarkers ]
timelineWindowSetBookmarks timelineWin (map fst usrMarkers)
if nevents == 0
then continueWith NoEventlogLoaded
else continueWith EventlogLoaded
{ mfilename = mfilename
, hecs = hecs
, selection = PointSelection 0
, cursorPos = 0
}
dispatch EventExportDialog
EventlogLoaded {mfilename} = do
exportFileDialog mainWin (fromMaybe "" mfilename) $ \filename' format ->
post (EventFileExport filename' format)
continue
dispatch (EventFileExport filename format)
EventlogLoaded {hecs} = do
viewParams <- timelineGetViewParameters timelineWin
let viewParams' = viewParams {
detail = 1,
bwMode = False,
labelsMode = False
}
let yScaleArea = timelineGetYScaleArea timelineWin
case format of
FormatPDF ->
saveAsPDF filename hecs viewParams' yScaleArea
FormatPNG ->
saveAsPNG filename hecs viewParams' yScaleArea
continue
dispatch EventLaunchWebsite _ = do
GtkExtras.launchProgramForURI "http://www.haskell.org/haskellwiki/ThreadScope"
continue
dispatch EventLaunchTutorial _ = do
GtkExtras.launchProgramForURI "http://www.haskell.org/haskellwiki/ThreadScope_Tour"
continue
dispatch EventAboutDialog _ = do
aboutDialog mainWin
continue
dispatch (EventShowSidebar visible) _ = do
MainWindow.sidebarSetVisibility mainWin visible
continue
dispatch (EventShowEvents visible) _ = do
MainWindow.eventsSetVisibility mainWin visible
continue
dispatch EventTimelineJumpStart _ = do
timelineScrollToBeginning timelineWin
eventsViewScrollToLine eventsView 0
continue
dispatch EventTimelineJumpEnd EventlogLoaded{hecs} = do
timelineScrollToEnd timelineWin
let (_,end) = bounds (hecEventArray hecs)
eventsViewScrollToLine eventsView end
continue
dispatch EventTimelineJumpCursor EventlogLoaded{cursorPos} = do
timelineCentreOnCursor timelineWin --TODO: pass selection here
eventsViewScrollToLine eventsView cursorPos
continue
dispatch EventTimelineScrollLeft _ = do
timelineScrollLeft timelineWin
continue
dispatch EventTimelineScrollRight _ = do
timelineScrollRight timelineWin
continue
dispatch EventTimelineZoomIn _ = do
timelineZoomIn timelineWin
continue
dispatch EventTimelineZoomOut _ = do
timelineZoomOut timelineWin
continue
dispatch EventTimelineZoomToFit _ = do
timelineZoomToFit timelineWin
continue
dispatch (EventTimelineLabelsMode labelsMode) _ = do
timelineSetLabelsMode timelineWin labelsMode
continue
dispatch (EventTimelineShowBW showBW) _ = do
timelineSetBWMode timelineWin showBW
continue
dispatch (EventCursorChangedIndex cursorPos') EventlogLoaded{hecs} = do
let cursorTs' = eventIndexToTimestamp hecs cursorPos'
selection' = PointSelection cursorTs'
mselection <- timelineSetSelection timelineWin selection'
setSelection cursorPos' Nothing mselection
dispatch (EventCursorChangedSelection selection'@(PointSelection cursorTs'))
EventlogLoaded{hecs} = do
let cursorPos' = timestampToEventIndex hecs cursorTs'
mselection <- timelineSetSelection timelineWin selection'
setSelection cursorPos' Nothing mselection
dispatch (EventCursorChangedSelection selection'@(RangeSelection start end))
EventlogLoaded{hecs} = do
let cursorPos' = timestampToEventIndex hecs start
mrange = Just (cursorPos', timestampToEventIndex hecs end)
mselection <- timelineSetSelection timelineWin selection'
setSelection cursorPos' mrange mselection
dispatch (EventTracesChanged traces) _ = do
timelineWindowSetTraces timelineWin traces
continue
dispatch EventBookmarkAdd EventlogLoaded{selection} = do
case selection of
PointSelection a -> bookmarkViewAdd bookmarkView a ""
RangeSelection a b -> do bookmarkViewAdd bookmarkView a ""
bookmarkViewAdd bookmarkView b ""
--TODO: should have a way to add/set a single bookmark for the timeline
-- rather than this hack where we ask the bookmark view for the whole lot.
ts <- bookmarkViewGet bookmarkView
timelineWindowSetBookmarks timelineWin (map fst ts)
continue
dispatch (EventBookmarkRemove n) _ = do
bookmarkViewRemove bookmarkView n
--TODO: should have a way to add/set a single bookmark for the timeline
-- rather than this hack where we ask the bookmark view for the whole lot.
ts <- bookmarkViewGet bookmarkView
timelineWindowSetBookmarks timelineWin (map fst ts)
continue
dispatch (EventBookmarkEdit n v) _ = do
bookmarkViewSetLabel bookmarkView n v
continue
dispatch (EventUserError doing exception) _ = do
let headline = "There was a problem " ++ doing ++ "."
explanation = show exception
errorMessageDialog mainWin headline explanation
continue
dispatch _ NoEventlogLoaded = continue
loadEvents mfilename registerEvents = do
ConcurrencyControl.fullSpeed concCtl $
ProgressView.withProgress mainWin $ \progress -> do
(hecs, name, nevents, timespan) <- registerEvents progress
-- This is a desperate hack to avoid the "segfault on reload" bug
-- http://trac.haskell.org/ThreadScope/ticket/1
-- It should be enough to let other threads finish and so avoid
-- re-entering gtk C code (see ticket for the dirty details).
--
-- Unfortunately it halts drawing of the loaded events if the user
-- manages to move the mouse away from the window during the delay.
-- threadDelay 100000 -- 1/10th of a second
post (EventSetState hecs mfilename name nevents timespan)
return ()
async doing action =
forkIO (action `catch` \e -> post (EventUserError doing e))
setSelection cursorPos' _ (Just selection'@(PointSelection _)) = do
eventsViewSetCursor eventsView cursorPos' Nothing
histogramViewSetInterval histogramView Nothing
summaryViewSetInterval summaryView Nothing
continueWith eventlogState {
selection = selection',
cursorPos = cursorPos'
}
setSelection cursorPos' mrange (Just selection'@(RangeSelection start end)) = do
eventsViewSetCursor eventsView cursorPos' mrange
histogramViewSetInterval histogramView (Just (start, end))
summaryViewSetInterval summaryView (Just (start, end))
continueWith eventlogState {
selection = selection',
cursorPos = cursorPos'
}
setSelection _ _ Nothing = continue
post = postEvent eventQueue
continue = continueWith eventlogState
continueWith = return . Right
-------------------------------------------------------------------------------
runGUI :: Maybe (Either FilePath String) -> IO ()
runGUI initialTrace = do
Gtk.initGUI
App.initApp
uiEnv <- constructUI
let post = postEvent (eventQueue uiEnv)
case initialTrace of
Nothing -> return ()
Just (Left filename) -> post (EventFileLoad filename)
Just (Right traceName) -> post (EventTestLoad traceName)
doneVar <- newEmptyMVar
forkIO $ do
res <- try $ eventLoop uiEnv NoEventlogLoaded
Gtk.mainQuit
putMVar doneVar (res :: Either SomeException ())
#ifndef mingw32_HOST_OS
installHandler sigINT (Catch $ post EventQuit) Nothing
#endif
-- Enter Gtk+ main event loop.
Gtk.mainGUI
-- Wait for child event loop to terminate
-- This lets us wait for any exceptions.
either throwIO return =<< takeMVar doneVar
================================================
FILE: GUI/MainWindow.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
module GUI.MainWindow (
MainWindow,
mainWindowNew,
MainWindowActions(..),
setFileLoaded,
setStatusMessage,
sidebarSetVisibility,
eventsSetVisibility,
) where
import Graphics.UI.Gtk as Gtk
import qualified System.Glib.GObject as Glib
import GUI.DataFiles (loadLogo)
-------------------------------------------------------------------------------
data MainWindow = MainWindow {
mainWindow :: Window,
sidebarBox,
eventsBox :: Widget,
statusBar :: Statusbar,
statusBarCxt :: ContextId
}
instance Glib.GObjectClass MainWindow where
toGObject = toGObject . mainWindow
unsafeCastGObject = error "cannot downcast to MainView type"
instance Gtk.WidgetClass MainWindow
instance Gtk.ContainerClass MainWindow
instance Gtk.BinClass MainWindow
instance Gtk.WindowClass MainWindow
data MainWindowActions = MainWindowActions {
-- Menu actions
mainWinOpen :: IO (),
mainWinExport :: IO (),
mainWinQuit :: IO (),
mainWinViewSidebar :: Bool -> IO (),
mainWinViewEvents :: Bool -> IO (),
mainWinViewBW :: Bool -> IO (),
mainWinViewReload :: IO (),
mainWinWebsite :: IO (),
mainWinTutorial :: IO (),
mainWinAbout :: IO (),
-- Toolbar actions
mainWinJumpStart :: IO (),
mainWinJumpEnd :: IO (),
mainWinJumpCursor :: IO (),
mainWinJumpZoomIn :: IO (),
mainWinJumpZoomOut :: IO (),
mainWinJumpZoomFit :: IO (),
mainWinScrollLeft :: IO (),
mainWinScrollRight :: IO (),
mainWinDisplayLabels :: Bool -> IO ()
}
-------------------------------------------------------------------------------
setFileLoaded :: MainWindow -> Maybe FilePath -> IO ()
setFileLoaded mainWin Nothing =
set (mainWindow mainWin) [
windowTitle := "ThreadScope"
]
setFileLoaded mainWin (Just file) =
set (mainWindow mainWin) [
windowTitle := file ++ " - ThreadScope"
]
setStatusMessage :: MainWindow -> String -> IO ()
setStatusMessage mainWin msg = do
statusbarPop (statusBar mainWin) (statusBarCxt mainWin)
statusbarPush (statusBar mainWin) (statusBarCxt mainWin) (' ':msg)
return ()
sidebarSetVisibility :: MainWindow -> Bool -> IO ()
sidebarSetVisibility mainWin visible =
set (sidebarBox mainWin) [ widgetVisible := visible ]
eventsSetVisibility :: MainWindow -> Bool -> IO ()
eventsSetVisibility mainWin visible =
set (eventsBox mainWin) [ widgetVisible := visible ]
-------------------------------------------------------------------------------
mainWindowNew :: Builder -> MainWindowActions -> IO MainWindow
mainWindowNew builder actions = do
let getWidget cast name = builderGetObject builder cast name
mainWindow <- getWidget castToWindow "main_window"
statusBar <- getWidget castToStatusbar "statusbar"
sidebarBox <- getWidget castToWidget "sidebar"
eventsBox <- getWidget castToWidget "eventsbox"
bwToggle <- getWidget castToCheckMenuItem "black_and_white"
labModeToggle <- getWidget castToCheckMenuItem "view_labels_mode"
sidebarToggle <- getWidget castToCheckMenuItem "view_sidebar"
eventsToggle <- getWidget castToCheckMenuItem "view_events"
openMenuItem <- getWidget castToMenuItem "openMenuItem"
exportMenuItem <- getWidget castToMenuItem "exportMenuItem"
reloadMenuItem <- getWidget castToMenuItem "view_reload"
quitMenuItem <- getWidget castToMenuItem "quitMenuItem"
websiteMenuItem <- getWidget castToMenuItem "websiteMenuItem"
tutorialMenuItem <- getWidget castToMenuItem "tutorialMenuItem"
aboutMenuItem <- getWidget castToMenuItem "aboutMenuItem"
firstMenuItem <- getWidget castToMenuItem "move_first"
centreMenuItem <- getWidget castToMenuItem "move_centre"
lastMenuItem <- getWidget castToMenuItem "move_last"
zoomInMenuItem <- getWidget castToMenuItem "move_zoomin"
zoomOutMenuItem <- getWidget castToMenuItem "move_zoomout"
zoomFitMenuItem <- getWidget castToMenuItem "move_zoomfit"
openButton <- getWidget castToToolButton "cpus_open"
firstButton <- getWidget castToToolButton "cpus_first"
centreButton <- getWidget castToToolButton "cpus_centre"
lastButton <- getWidget castToToolButton "cpus_last"
zoomInButton <- getWidget castToToolButton "cpus_zoomin"
zoomOutButton <- getWidget castToToolButton "cpus_zoomout"
zoomFitButton <- getWidget castToToolButton "cpus_zoomfit"
------------------------------------------------------------------------
-- Show everything
widgetShowAll mainWindow
------------------------------------------------------------------------
logo <- $loadLogo
set mainWindow [ windowIcon := logo ]
------------------------------------------------------------------------
-- Status bar functionality
statusBarCxt <- statusbarGetContextId statusBar "file"
statusbarPush statusBar statusBarCxt "No eventlog loaded."
------------------------------------------------------------------------
-- Bind all the events
-- Menus
on openMenuItem menuItemActivate $ mainWinOpen actions
on exportMenuItem menuItemActivate $ mainWinExport actions
on quitMenuItem menuItemActivate $ mainWinQuit actions
on mainWindow objectDestroy $ mainWinQuit actions
on sidebarToggle checkMenuItemToggled $ checkMenuItemGetActive sidebarToggle
>>= mainWinViewSidebar actions
on eventsToggle checkMenuItemToggled $ checkMenuItemGetActive eventsToggle
>>= mainWinViewEvents actions
on bwToggle checkMenuItemToggled $ checkMenuItemGetActive bwToggle
>>= mainWinViewBW actions
on labModeToggle checkMenuItemToggled $ checkMenuItemGetActive labModeToggle
>>= mainWinDisplayLabels actions
on reloadMenuItem menuItemActivate $ mainWinViewReload actions
on websiteMenuItem menuItemActivate $ mainWinWebsite actions
on tutorialMenuItem menuItemActivate $ mainWinTutorial actions
on aboutMenuItem menuItemActivate $ mainWinAbout actions
on firstMenuItem menuItemActivate $ mainWinJumpStart actions
on centreMenuItem menuItemActivate $ mainWinJumpCursor actions
on lastMenuItem menuItemActivate $ mainWinJumpEnd actions
on zoomInMenuItem menuItemActivate $ mainWinJumpZoomIn actions
on zoomOutMenuItem menuItemActivate $ mainWinJumpZoomOut actions
on zoomFitMenuItem menuItemActivate $ mainWinJumpZoomFit actions
-- Toolbar
onToolButtonClicked openButton $ mainWinOpen actions
onToolButtonClicked firstButton $ mainWinJumpStart actions
onToolButtonClicked centreButton $ mainWinJumpCursor actions
onToolButtonClicked lastButton $ mainWinJumpEnd actions
onToolButtonClicked zoomInButton $ mainWinJumpZoomIn actions
onToolButtonClicked zoomOutButton $ mainWinJumpZoomOut actions
onToolButtonClicked zoomFitButton $ mainWinJumpZoomFit actions
return MainWindow {..}
================================================
FILE: GUI/ProgressView.hs
================================================
{-# LANGUAGE DeriveDataTypeable #-}
module GUI.ProgressView (
ProgressView,
withProgress,
setText,
setTitle,
setProgress,
startPulse,
) where
import Graphics.Rendering.Cairo
import Graphics.UI.Gtk as Gtk
import GUI.GtkExtras
import qualified Control.Concurrent as Concurrent
import Control.Exception
import Data.Typeable
import Control.Monad.Trans
data ProgressView = ProgressView {
progressWindow :: Gtk.Window,
progressLabel :: Gtk.Label,
progressBar :: Gtk.ProgressBar
}
-- | Perform a long-running operation and display a progress window. The
-- operation has access to the progress window and it is expected to update it
-- using 'setText' and 'setProgress'
--
-- The user may cancel the operation at any time.
--
withProgress :: WindowClass win => win -> (ProgressView -> IO a) -> IO (Maybe a)
withProgress parent action = do
self <- Concurrent.myThreadId
let cancel = throwTo self OperationInterrupted
bracket (new parent cancel) close $ \progress ->
fmap Just (action progress)
`catch` \OperationInterrupted -> return Nothing
data OperationInterrupted = OperationInterrupted
deriving (Typeable, Show)
instance Exception OperationInterrupted
setText :: ProgressView -> String -> IO ()
setText view msg =
set (progressBar view) [
progressBarText := msg
]
setTitle :: ProgressView -> String -> IO ()
setTitle view msg = do
set (progressWindow view) [ windowTitle := msg ]
set (progressLabel view) [ labelLabel := "<b>" ++ msg ++ "</b>" ]
startPulse :: ProgressView -> IO (IO ())
startPulse view = do
let pulse = do
progressBarPulse (progressBar view)
Concurrent.threadDelay 200000
pulse
thread <- Concurrent.forkIO $
pulse `catch` \OperationInterrupted -> return ()
let stop = throwTo thread OperationInterrupted
waitGUI
return stop
setProgress :: ProgressView -> Int -> Int -> IO ()
setProgress view total current = do
let frac = fromIntegral current / fromIntegral total
set (progressBar view) [ progressBarFraction := frac ]
waitGUI
close :: ProgressView -> IO ()
close view = widgetDestroy (progressWindow view)
new :: WindowClass win => win -> IO () -> IO ProgressView
new parent cancelAction = do
win <- windowNew
set win [
containerBorderWidth := 10,
windowTitle := "",
windowTransientFor := toWindow parent,
windowModal := True,
windowWindowPosition := WinPosCenterOnParent,
windowDefaultWidth := 400,
windowSkipTaskbarHint := True
]
progText <- labelNew (Nothing :: Maybe String)
set progText [
miscXalign := 0,
labelUseMarkup := True
]
progress <- progressBarNew
cancel <- buttonNewFromStock stockCancel
cancel `on` buttonActivated $ (widgetDestroy win >> cancelAction)
win `on` destroyEvent $ lift cancelAction >> return True
on win keyPressEvent $ do
keyVal <- eventKeyVal
case keyVal of
0xff1b -> liftIO $ cancelAction >> return True
_ -> return False
vbox <- vBoxNew False 20
hbox <- hBoxNew False 0
boxPackStart vbox progText PackRepel 10
boxPackStart vbox progress PackGrow 5
boxPackStart vbox hbox PackNatural 5
boxPackEnd hbox cancel PackNatural 0
containerAdd win vbox
widgetShowAll win
return ProgressView {
progressWindow = win,
progressLabel = progText,
progressBar = progress
}
================================================
FILE: GUI/SaveAs.hs
================================================
module GUI.SaveAs (saveAsPDF, saveAsPNG) where
-- Imports for ThreadScope
import GUI.Timeline.Render (renderTraces, renderYScaleArea)
import GUI.Timeline.Render.Constants
import GUI.Timeline.Ticks (renderXScaleArea)
import GUI.Types
import Events.HECs
-- Imports for GTK
import Graphics.UI.Gtk hiding (rectangle)
import Graphics.Rendering.Cairo
( Render
, Operator(..)
, Format(..)
, rectangle
, getOperator
, setOperator
, fill
, translate
, liftIO
, withPDFSurface
, renderWith
, withImageSurface
, surfaceWriteToPNG
)
saveAs :: HECs -> ViewParameters -> Double -> DrawingArea
-> (Int, Int, Render ())
saveAs hecs params'@ViewParameters{xScaleAreaHeight, width,
height = oldHeight {-, histogramHeight-}}
yScaleAreaWidth yScaleArea =
let histTotalHeight = histXScaleHeight -- + histogramHeight
params@ViewParameters{height} =
params'{ viewTraces = viewTraces params' -- ++ [TraceHistogram]
, height = oldHeight + histTotalHeight + tracePad
}
w = ceiling yScaleAreaWidth + width
h = xScaleAreaHeight + height
drawTraces = renderTraces params hecs (Rectangle 0 0 width height)
drawXScale = renderXScaleArea params hecs
drawYScale = renderYScaleArea params hecs yScaleArea
-- Functions renderTraces and renderXScaleArea draw to the left of 0
-- which is not seen in the normal mode, but would be seen in export,
-- so it has to be cleared before renderYScaleArea is written on top:
clearLeftArea = do
rectangle 0 0 yScaleAreaWidth (fromIntegral h)
op <- getOperator
setOperator OperatorClear
fill
setOperator op
drawAll = do
translate yScaleAreaWidth (fromIntegral xScaleAreaHeight)
drawTraces
translate 0 (- fromIntegral xScaleAreaHeight)
drawXScale
translate (-yScaleAreaWidth) 0
clearLeftArea
translate 0 (fromIntegral xScaleAreaHeight)
drawYScale
in (w, h, drawAll)
saveAsPDF :: FilePath -> HECs -> ViewParameters -> DrawingArea -> IO ()
saveAsPDF filename hecs params yScaleArea = do
Rectangle _ _ xoffset _ <- liftIO $ widgetGetAllocation yScaleArea
let (w', h', drawAll) = saveAs hecs params (fromIntegral xoffset) yScaleArea
withPDFSurface filename (fromIntegral w') (fromIntegral h') $ \surface ->
renderWith surface drawAll
saveAsPNG :: FilePath -> HECs -> ViewParameters -> DrawingArea -> IO ()
saveAsPNG filename hecs params yScaleArea = do
Rectangle _ _ xoffset _ <- liftIO $ widgetGetAllocation yScaleArea
let (w', h', drawAll) = saveAs hecs params (fromIntegral xoffset) yScaleArea
withImageSurface FormatARGB32 w' h' $ \surface -> do
renderWith surface drawAll
surfaceWriteToPNG surface filename
================================================
FILE: GUI/StartupInfoView.hs
================================================
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module GUI.StartupInfoView (
StartupInfoView,
startupInfoViewNew,
startupInfoViewSetEvents,
) where
import GHC.RTS.Events
import Graphics.UI.Gtk
import qualified Graphics.UI.Gtk.ModelView.TreeView.Compat as Compat
import Data.Array
import Data.List
import Data.Maybe
import Data.Time
import Data.Time.Clock.POSIX
import Data.Text (Text)
import qualified Data.Text as T
-------------------------------------------------------------------------------
data StartupInfoView = StartupInfoView
{ labelProgName :: Label
, storeProgArgs :: ListStore Text
, storeProgEnv :: ListStore (Text, Text)
, labelProgStartTime :: Label
, labelProgRtsId :: Label
}
data StartupInfoState
= StartupInfoEmpty
| StartupInfoLoaded
{ progName :: Maybe Text
, progArgs :: Maybe [Text]
, progEnv :: Maybe [(Text, Text)]
, progStartTime :: Maybe UTCTime
, progRtsId :: Maybe Text
}
-------------------------------------------------------------------------------
startupInfoViewNew :: Builder -> IO StartupInfoView
startupInfoViewNew builder = do
let getWidget cast = builderGetObject builder cast
labelProgName <- getWidget castToLabel ("labelProgName" :: Text)
treeviewProgArgs <- getWidget castToTreeView ("treeviewProgArguments" :: Text)
treeviewProgEnv <- getWidget castToTreeView ("treeviewProgEnvironment" :: Text)
labelProgStartTime <- getWidget castToLabel ("labelProgStartTime" :: Text)
labelProgRtsId <- getWidget castToLabel ("labelProgRtsIdentifier" :: Text)
storeProgArgs <- listStoreNew []
columnArgs <- treeViewColumnNew
cellArgs <- cellRendererTextNew
treeViewColumnPackStart columnArgs cellArgs True
treeViewAppendColumn treeviewProgArgs columnArgs
Compat.treeViewSetModel treeviewProgArgs (Just storeProgArgs)
set cellArgs [ cellTextEditable := True ]
cellLayoutSetAttributes columnArgs cellArgs storeProgArgs $ \arg ->
[ cellText := arg ]
storeProgEnv <- listStoreNew []
columnVar <- treeViewColumnNew
cellVar <- cellRendererTextNew
columnValue <- treeViewColumnNew
cellValue <- cellRendererTextNew
treeViewColumnPackStart columnVar cellVar False
treeViewColumnPackStart columnValue cellValue True
treeViewAppendColumn treeviewProgEnv columnVar
treeViewAppendColumn treeviewProgEnv columnValue
Compat.treeViewSetModel treeviewProgEnv (Just storeProgEnv)
cellLayoutSetAttributes columnVar cellVar storeProgEnv $ \(var,_) ->
[ cellText := var ]
set cellValue [ cellTextEditable := True ]
cellLayoutSetAttributes columnValue cellValue storeProgEnv $ \(_,value) ->
[ cellText := value ]
let startupInfoView = StartupInfoView{..}
return startupInfoView
-------------------------------------------------------------------------------
startupInfoViewSetEvents :: StartupInfoView -> Maybe (Array Int Event) -> IO ()
startupInfoViewSetEvents view mevents =
updateStartupInfo view (maybe StartupInfoEmpty processEvents mevents)
--TODO: none of this handles the possibility of an eventlog containing multiple
-- OS processes. Note that the capset arg is ignored in the events below.
processEvents :: Array Int Event -> StartupInfoState
processEvents = foldl' accum (StartupInfoLoaded Nothing Nothing Nothing Nothing Nothing)
. take 1000
. elems
where
accum info (Event _ (ProgramArgs _ (name:args)) _) =
info {
progName = Just name,
progArgs = Just args
}
accum info (Event _ (ProgramEnv _ env) _) =
info { progEnv = Just (sort (parseEnv env)) }
accum info (Event _ (RtsIdentifier _ rtsid) _) =
info { progRtsId = Just rtsid }
accum info (Event timestamp (WallClockTime _ sec nsec) _) =
-- WallClockTime records the wall clock time of *this* event
-- which occurs some time after startup, so we can just subtract
-- the timestamp since that is the relative time since startup.
let wallTimePosix :: NominalDiffTime
wallTimePosix = fromIntegral sec
+ (fromIntegral nsec / nanoseconds)
- (fromIntegral timestamp / nanoseconds)
nanoseconds = 1000000000
wallTimeUTC = posixSecondsToUTCTime wallTimePosix
in info { progStartTime = Just wallTimeUTC }
accum info _ = info
-- convert ["foo=bar", ...] to [("foo", "bar"), ...]
parseEnv env = [ (var, value) | (var, T.drop 1 -> value) <- map (T.span (/='=')) env ]
updateStartupInfo :: StartupInfoView -> StartupInfoState -> IO ()
updateStartupInfo StartupInfoView{..} StartupInfoLoaded{..} = do
set labelProgName [ labelText := fromMaybe "(unknown)" progName ]
set labelProgStartTime [ labelText := maybe "(unknown)" show progStartTime ]
set labelProgRtsId [ labelText := fromMaybe "(unknown)" progRtsId ]
listStoreClear storeProgArgs
mapM_ (listStoreAppend storeProgArgs) (fromMaybe [] progArgs)
listStoreClear storeProgEnv
mapM_ (listStoreAppend storeProgEnv) (fromMaybe [] progEnv)
updateStartupInfo StartupInfoView{..} StartupInfoEmpty = do
set labelProgName [ labelText := ("" :: Text) ]
set labelProgStartTime [ labelText := ("" :: Text) ]
set labelProgRtsId [ labelText := ("" :: Text) ]
listStoreClear storeProgArgs
listStoreClear storeProgEnv
================================================
FILE: GUI/SummaryView.hs
================================================
module GUI.SummaryView (
SummaryView,
summaryViewNew,
summaryViewSetEvents,
summaryViewSetInterval,
) where
import GHC.RTS.Events
import GUI.Types
import Graphics.UI.Gtk
import qualified Graphics.UI.Gtk.ModelView.TreeView.Compat as Compat
import Control.Exception (assert)
import Control.Monad
import Data.Array
import qualified Data.IntMap as IM
import Data.IORef
import Data.List as L
import Data.Maybe
import Data.Word (Word64)
import Numeric (showFFloat)
import Text.Printf
------------------------------------------------------------------------------
type Events = Array Int Event
data SummaryView = SummaryView {
-- we cache the stats for the whole interval
cacheEventsStats :: !(IORef (Maybe (Events, SummaryStats, Bool)))
-- widgets for time stuff
, labelTimeTotal :: Label
, labelTimeMutator :: Label
, labelTimeGC :: Label
, labelTimeProductivity :: Label
-- widgets for heap stuff
, labelHeapMaxSize
, labelHeapMaxResidency
, labelHeapAllocTotal
, labelHeapAllocRate
, labelHeapMaxSlop :: (Label, Label, Label, Label)
, tableHeap :: Widget
-- widgets for GC stuff
, labelGcCopied :: (Label, Label, Label, Label)
, labelGcParWorkBalance :: Label
, storeGcStats :: ListStore GcStatsEntry
, tableGc :: Widget
-- widgets for sparks stuff
, storeSparkStats :: ListStore (Cap, SparkCounts)
}
------------------------------------------------------------------------------
summaryViewNew :: Builder -> IO SummaryView
summaryViewNew builder = do
cacheEventsStats <- newIORef Nothing
let getWidget cast = builderGetObject builder cast
getLabel = getWidget castToLabel
getHeapLabels w1 w2 w3 w4 = liftM4 (,,,) (getLabel w1) (getLabel w2)
(getLabel w3) (getLabel w4)
labelTimeTotal <- getWidget castToLabel "labelTimeTotal"
labelTimeMutator <- getWidget castToLabel "labelTimeMutator"
labelTimeGC <- getWidget castToLabel "labelTimeGC"
labelTimeProductivity <- getWidget castToLabel "labelTimeProductivity"
labelHeapMaxSize <- getHeapLabels "labelHeapMaxSize" "labelHeapMaxSizeUnit"
"labelHeapMaxSizeBytes" "labelHeapMaxSizeUnit1"
labelHeapMaxResidency <- getHeapLabels "labelHeapMaxResidency" "labelHeapMaxResidencyUnit"
"labelHeapMaxResidencyBytes" "labelHeapMaxResidencyUnit1"
labelHeapAllocTotal <- getHeapLabels "labelHeapAllocTotal" "labelHeapAllocTotalUnit"
"labelHeapAllocTotalBytes" "labelHeapAllocTotalUnit1"
labelHeapAllocRate <- getHeapLabels "labelHeapAllocRate" "labelHeapAllocRateUnit"
"labelHeapAllocRateBytes" "labelHeapAllocRateUnit1"
labelHeapMaxSlop <- getHeapLabels "labelHeapMaxSlop" "labelHeapMaxSlopUnit"
"labelHeapMaxSlopBytes" "labelHeapMaxSlopUnit1"
tableHeap <- getWidget castToWidget "tableHeap"
labelGcCopied <- getHeapLabels "labelGcCopied" "labelGcCopiedUnit"
"labelGcCopiedBytes" "labelGcCopiedUnit1"
labelGcParWorkBalance <- getWidget castToLabel "labelGcParWorkBalance"
storeGcStats <- listStoreNew []
tableGc <- getWidget castToWidget "tableGC"
storeSparkStats <- listStoreNew []
let summaryView = SummaryView{..}
treeviewGcStats <- getWidget castToTreeView "treeviewGcStats"
Compat.treeViewSetModel treeviewGcStats (Just storeGcStats)
let addGcColumn = addColumn treeviewGcStats storeGcStats
addGcColumn "Generation" $ \(GcStatsEntry gen _ _ _ _ _) ->
[ cellText := if gen == -1 then "GC Total" else "Gen " ++ show gen ]
addGcColumn "Collections" $ \(GcStatsEntry _ colls _ _ _ _) ->
[ cellText := show colls ]
addGcColumn "Par collections" $ \(GcStatsEntry _ _ pcolls _ _ _) ->
[ cellText := show pcolls ]
addGcColumn "Elapsed time" $ \(GcStatsEntry _ _ _ time _ _) ->
[ cellText := (printf "%5.2fs" (timeToSecondsDbl time) :: String) ]
addGcColumn "Avg pause" $ \(GcStatsEntry _ _ _ _ avgpause _) ->
[ cellText := (printf "%3.4fs" avgpause :: String) ]
addGcColumn "Max pause" $ \(GcStatsEntry _ _ _ _ _ maxpause) ->
[ cellText := (printf "%3.4fs" maxpause :: String) ]
treeviewSparkStats <- getWidget castToTreeView "treeviewSparkStats"
Compat.treeViewSetModel treeviewSparkStats (Just storeSparkStats)
let addSparksColumn = addColumn treeviewSparkStats storeSparkStats
addSparksColumn "HEC" $ \(hec, _) ->
[ cellText := if hec == -1 then "Total" else "HEC " ++ show hec ]
addSparksColumn "Total" $ \(_, SparkCounts total _ _ _ _ _) ->
[ cellText := show total ]
addSparksColumn "Converted" $ \(_, SparkCounts _ conv _ _ _ _) ->
[ cellText := show conv ]
addSparksColumn "Overflowed" $ \(_, SparkCounts _ _ ovf _ _ _) ->
[ cellText := show ovf ]
addSparksColumn "Dud" $ \(_, SparkCounts _ _ _ dud _ _) ->
[ cellText := show dud ]
addSparksColumn "GCed" $ \(_, SparkCounts _ _ _ _ gc _) ->
[ cellText := show gc ]
addSparksColumn "Fizzled" $ \(_, SparkCounts _ _ _ _ _ fiz) ->
[ cellText := show fiz ]
return summaryView
where
addColumn view store title mkAttrs = do
col <- treeViewColumnNew
cell <- cellRendererTextNew
treeViewColumnSetTitle col title
treeViewColumnPackStart col cell False
treeViewAppendColumn view col
cellLayoutSetAttributes col cell store mkAttrs
------------------------------------------------------------------------------
summaryViewSetEvents :: SummaryView -> Maybe (Array Int Event) -> IO ()
summaryViewSetEvents view@SummaryView{cacheEventsStats} Nothing = do
writeIORef cacheEventsStats Nothing
setSummaryStatsEmpty view
summaryViewSetEvents view@SummaryView{cacheEventsStats} (Just events) = do
let stats = summaryStats events Nothing
-- this is an almost certain indicator that there
-- are no heap events in the eventlog:
hasHeapEvents = heapMaxSize (summHeapStats stats) /= Just 0
writeIORef cacheEventsStats (Just (events, stats, hasHeapEvents))
setSummaryStats view stats hasHeapEvents
summaryViewSetInterval :: SummaryView -> Maybe Interval -> IO ()
summaryViewSetInterval view@SummaryView{cacheEventsStats} Nothing = do
cache <- readIORef cacheEventsStats
case cache of
Nothing -> return ()
Just (_, stats, hasHeap) -> setSummaryStats view stats hasHeap
summaryViewSetInterval view@SummaryView{cacheEventsStats} (Just interval) = do
cache <- readIORef cacheEventsStats
case cache of
Nothing -> return ()
Just (events, _, hasHeap) -> setSummaryStats view stats hasHeap
where stats = summaryStats events (Just interval)
------------------------------------------------------------------------------
setSummaryStats :: SummaryView -> SummaryStats -> Bool -> IO ()
setSummaryStats view SummaryStats{..} hasHeapEvents = do
setTimeStats view summTimeStats
if hasHeapEvents
then do setHeapStatsAvailable view True
setHeapStats view summHeapStats
setGcStats view summGcStats
else setHeapStatsAvailable view False
setSparkStats view summSparkStats
setTimeStats :: SummaryView -> TimeStats -> IO ()
setTimeStats SummaryView{..} TimeStats{..} =
mapM_ (\(label, text) -> set label [ labelText := text ])
[ (labelTimeTotal , showTimeWithUnit timeTotal)
, (labelTimeMutator , showTimeWithUnit timeMutator)
, (labelTimeGC , showTimeWithUnit timeGC)
, (labelTimeProductivity, showFFloat (Just 1) (timeProductivity * 100) "% of mutator vs total")
]
setHeapStats :: SummaryView -> HeapStats -> IO ()
setHeapStats SummaryView{..} HeapStats{..} = do
setHeapStatLabels labelHeapMaxSize heapMaxSize "" ""
setHeapStatLabels labelHeapMaxResidency heapMaxResidency "" ""
setHeapStatLabels labelHeapAllocTotal heapTotalAlloc "" ""
setHeapStatLabels labelHeapAllocRate heapAllocRate "/s" " per second (of mutator time)"
setHeapStatLabels labelHeapMaxSlop heapMaxSlop "" ""
setHeapStatLabels labelGcCopied heapCopiedDuringGc "" ""
where
setHeapStatLabels labels stat unitSuffix unitSuffixLong =
let texts = case stat of
Nothing -> ("N/A", "", "", "")
Just b -> ( formatBytesInUnit b u, formatUnit u ++ unitSuffix
, formatBytes b, "bytes" ++ unitSuffixLong)
where u = getByteUnit b
in setLabels labels texts
setLabels (short,shortunit,long,longunit) (short', shortunit', long', longunit') = do
mapM_ (\(label, text) -> set label [ labelText := text ])
[ (short, short'), (shortunit, shortunit')
, (long, long'), (longunit, longunit') ]
setGcStats :: SummaryView -> GcStats -> IO ()
setGcStats SummaryView{..} GcStats{..} = do
let balText = maybe "N/A"
(printf "%.2f%% (serial 0%%, perfect 100%%)")
gcParWorkBalance
set labelGcParWorkBalance [ labelText := balText ]
listStoreClear storeGcStats
mapM_ (listStoreAppend storeGcStats) (gcTotalStats:gcGenStats)
setSparkStats :: SummaryView -> SparkStats -> IO ()
setSparkStats SummaryView{..} SparkStats{..} = do
listStoreClear storeSparkStats
mapM_ (listStoreAppend storeSparkStats) ((-1,totalSparkStats):capSparkStats)
data ByteUnit = TiB | GiB | MiB | KiB | B deriving Show
byteUnitVal :: ByteUnit -> Word64
byteUnitVal TiB = 2^40
byteUnitVal GiB = 2^30
byteUnitVal MiB = 2^20
byteUnitVal KiB = 2^10
byteUnitVal B = 1
getByteUnit :: Word64 -> ByteUnit
getByteUnit b
| b >= 2^40 = TiB
| b >= 2^30 = GiB
| b >= 2^20 = MiB
| b >= 2^10 = KiB
| otherwise = B
formatBytesInUnit :: Word64 -> ByteUnit -> String
formatBytesInUnit n u =
formatFixed (fromIntegral n / fromIntegral (byteUnitVal u))
where
formatFixed x = showFFloat (Just 1) x ""
formatUnit :: ByteUnit -> String
formatUnit = show
formatBytes :: Word64 -> String
formatBytes b = ppWithCommas b
ppWithCommas :: Word64 -> String
ppWithCommas =
let spl [] = []
spl l = let (c3, cs) = L.splitAt 3 l
in c3 : spl cs
in L.reverse . L.intercalate "," . spl . L.reverse . show
setSummaryStatsEmpty :: SummaryView -> IO ()
setSummaryStatsEmpty SummaryView{..} = do
mapM_ (\label -> set label [ labelText := ""
, widgetTooltipText
:= (Nothing :: Maybe String) ]) $
[ labelTimeTotal, labelTimeMutator
, labelTimeGC, labelTimeProductivity ] ++
[ w
| (a,b,c,d) <- [ labelHeapMaxSize, labelHeapMaxResidency
, labelHeapAllocTotal, labelHeapAllocRate
, labelHeapMaxSlop, labelGcCopied ]
, w <- [ a,b,c,d] ]
listStoreClear storeGcStats
listStoreClear storeSparkStats
setHeapStatsAvailable :: SummaryView -> Bool -> IO ()
setHeapStatsAvailable SummaryView{..} available
| available = do
forM_ unavailableWidgets $ \widget ->
set widget [ widgetTooltipText := (Nothing :: Maybe String)
, widgetSensitive := True ]
| otherwise = do
forM_ allLabels $ \label -> set label [ labelText := "" ]
listStoreClear storeGcStats
forM_ unavailableLabels $ \label ->
set label [ labelText := "(unavailable)" ]
forM_ unavailableWidgets $ \widget ->
set widget [ widgetTooltipText := Just msgInfoUnavailable, widgetSensitive := False ]
where
allLabels =
[ labelTimeMutator, labelTimeGC
, labelTimeProductivity, labelGcParWorkBalance ] ++
[ w | (a,b,c,d) <- [ labelHeapMaxSize, labelHeapMaxResidency
, labelHeapAllocTotal, labelHeapAllocRate
, labelHeapMaxSlop, labelGcCopied ]
, w <- [ a,b,c,d] ]
unavailableLabels =
[ labelTimeMutator, labelTimeGC
, labelTimeProductivity, labelGcParWorkBalance
, case labelGcCopied of (w,_,_,_) -> w ] ++
[ c | (_,_,c,_) <- [ labelHeapMaxSize, labelHeapMaxResidency
, labelHeapAllocTotal, labelHeapAllocRate
, labelHeapMaxSlop ] ]
unavailableWidgets = [ toWidget labelTimeMutator, toWidget labelTimeGC
, toWidget labelTimeProductivity
, tableHeap, tableGc ]
msgInfoUnavailable = "This eventlog does not contain heap or GC information."
------------------------------------------------------------------------------
-- Calculating the stats we want to display
--
data SummaryStats = SummaryStats {
summTimeStats :: TimeStats,
summHeapStats :: HeapStats,
summGcStats :: GcStats,
summSparkStats :: SparkStats
}
data TimeStats = TimeStats {
timeTotal :: !Word64, -- we really should have a better type for elapsed time
timeGC :: !Word64,
timeMutator :: !Word64,
timeProductivity :: !Double
}
data HeapStats = HeapStats {
heapMaxSize :: Maybe Word64,
heapMaxResidency :: Maybe Word64,
heapMaxSlop :: Maybe Word64,
heapTotalAlloc :: Maybe Word64,
heapAllocRate :: Maybe Word64,
heapCopiedDuringGc :: Maybe Word64
}
data GcStats = GcStats {
gcNumThreads :: !Int,
gcParWorkBalance :: !(Maybe Double),
gcGenStats :: [GcStatsEntry],
gcTotalStats :: !GcStatsEntry
}
data GcStatsEntry = GcStatsEntry !Int !Int !Int !Word64 !Double !Double
data SparkStats = SparkStats {
capSparkStats :: [(Cap, SparkCounts)],
totalSparkStats :: !SparkCounts
}
data SparkCounts = SparkCounts !Word64 !Word64 !Word64 !Word64 !Word64 !Word64
-- | Take the events, and optionally some sub-range, and generate the summary
-- stats for that range.
--
-- We take a two-step approach:
-- * a single pass over the events, accumulating into an intermediate
-- 'StatsAccum' record,
-- * then look at that 'StatsAccum' record and construct the various final
-- stats that we want to present.
--
summaryStats :: Array Int Event -> Maybe Interval -> SummaryStats
summaryStats events minterval =
SummaryStats {
summHeapStats = hs,
summGcStats = gs,
summSparkStats = ss,
summTimeStats = ts
}
where
!statsAccum = accumStats events minterval
gs = gcStats statsAccum
ss = sparkStats statsAccum
ts = timeStats events minterval gs
hs = heapStats statsAccum ts
-- | Linearly accumulate the stats from the events array,
-- either the full thing or some sub-range.
accumStats :: Array Int Event -> Maybe Interval -> StatsAccum
accumStats events minterval =
foldl' accumEvent start [ events ! i | i <- range eventsRange ]
where
eventsRange = selectEventRange events minterval
-- If we're starting from time zero then we know many of the stats
-- also start at from, where as from other points it's just unknown
start | fst eventsRange == 0 = zeroStatsAccum
| otherwise = emptyStatsAccum
-- | Given the event array and a time interval, return the range of array
-- indicies containing that interval. The Nothing interval means to select
-- the whole array range.
--
selectEventRange :: Array Int Event -> Maybe Interval -> (Int, Int)
selectEventRange arr Nothing = bounds arr
selectEventRange arr (Just (start, end)) = (lbound, ubound)
where
!lbound = either snd id $ findArrayRange cmp arr start
!ubound = either fst id $ findArrayRange cmp arr end
cmp ts (Event ts' _ _) = compare ts ts'
findArrayRange :: (key -> val -> Ordering)
-> Array Int val -> key -> Either (Int,Int) Int
findArrayRange cmp arr key =
binarySearch a0 b0 key
where
(a0,b0) = bounds arr
binarySearch a b key
| a > b = Left (b,a)
| otherwise = case cmp key (arr ! mid) of
LT -> binarySearch a (mid-1) key
EQ -> Right mid
GT -> binarySearch (mid+1) b key
where mid = (a + b) `div` 2
------------------------------------------------------------------------------
-- Final step where we convert from StatsAccum to various presentation forms
timeStats :: Array Int Event -> Maybe Interval -> GcStats -> TimeStats
timeStats events minterval
GcStats { gcTotalStats = GcStatsEntry _ _ _ timeGC _ _ } =
TimeStats {..}
where
timeTotal = intervalEnd - intervalStart
timeMutator = timeTotal - timeGC
timeProductivity = timeToSecondsDbl timeMutator
/ timeToSecondsDbl timeTotal
(intervalStart, intervalEnd) =
case minterval of
Just (s,e) -> (s, e)
Nothing -> (0, evTime (events ! ub))
where
(_lb, ub) = bounds events
heapStats :: StatsAccum -> TimeStats -> HeapStats
heapStats StatsAccum{..} TimeStats{timeMutator} =
HeapStats {
heapMaxSize = dmaxMemory,
heapMaxResidency = dmaxResidency,
heapMaxSlop = dmaxSlop,
heapTotalAlloc = if totalAlloc == 0
then Nothing
else Just totalAlloc,
heapAllocRate = if timeMutator == 0 || totalAlloc == 0
then Nothing
else Just $ truncate (fromIntegral totalAlloc / timeToSecondsDbl timeMutator),
heapCopiedDuringGc = if dcopied == Just 0
then Nothing
else dcopied
}
where
totalAlloc = sum [ end - start
| (end,start) <- IM.elems dallocTable ]
gcStats :: StatsAccum -> GcStats
gcStats StatsAccum{..} =
GcStats {
gcNumThreads = nThreads,
gcParWorkBalance,
gcGenStats = [ mkGcStatsEntry gen (gcGather gen)
| gen <- gens ],
gcTotalStats = mkGcStatsEntry gcGenTot (gcGather gcGenTot)
}
where
nThreads = fromMaybe 1 dmaxParNThreads
gcParWorkBalance | nThreads <= 1
|| fromMaybe 0 dparMaxCopied <= 0 = Nothing
| otherwise =
Just $
100 * ((maybe 0 fromIntegral dparTotCopied
/ maybe 0 fromIntegral dparMaxCopied) - 1)
/ (fromIntegral nThreads - 1)
gens = [0..maxGeneration]
where
-- Does not work for generationless GCs, but works reasonably
-- for > 2 gens and perfectly for 2 gens.
maxGeneration = maximum $ 1
: [ maxGen
| RtsGC { gcGenStat } <- IM.elems dGCTable
, not (IM.null gcGenStat)
, let (maxGen, _) = IM.findMax gcGenStat ]
gcGather :: Gen -> GenStat
gcGather gen = gcSum gen $ map gcGenStat $ IM.elems dGCTable
-- TODO: Consider per-HEC display of GC stats and then use
-- the values summed over all generations at key gcGenTot at each cap.
gcSum :: Gen -> [IM.IntMap GenStat] -> GenStat
gcSum gen l =
GenStat (sumPr gcAll) (sumPr gcPar)
(gcElapsed mainGen) (gcMaxPause mainGen)
where
l_genGC = map (IM.findWithDefault emptyGenStat gen) l
sumPr proj = sum $ map proj l_genGC
_maxPr proj = L.maximum $ map proj l_genGC
_minPr proj = L.minimum $ filter (> 0) $ map proj l_genGC
-- This would be the most balanced way of aggregating gcElapsed,
-- if only the event times were accurate.
_avgPr proj = let vs = filter (> 0) $ map proj l_genGC
in sum vs `div` fromIntegral (length vs)
-- But since the times include scheduling noise,
-- we only use the times from the main cap for each GC
-- and so get readings almost identical to +RTS -s.
mainGen = IM.findWithDefault emptyGenStat gen mainStat
mainStat = gcGenStat (fromMaybe (defaultGC 0) dGCMain)
mkGcStatsEntry :: Gen -> GenStat -> GcStatsEntry
mkGcStatsEntry gen GenStat{..} =
GcStatsEntry gen gcAll gcPar gcElapsedS gcAvgPauseS gcMaxPauseS
where
gcElapsedS = gcElapsed
gcMaxPauseS = timeToSecondsDbl gcMaxPause
gcAvgPauseS
| gcAll == 0 = 0
| otherwise = timeToSeconds $
fromIntegral gcElapsed / fromIntegral gcAll
sparkStats :: StatsAccum -> SparkStats
sparkStats StatsAccum{dsparkTable} =
SparkStats {
capSparkStats =
[ (cap, mkSparkStats sparkCounts)
| (cap, sparkCounts) <- capsSparkCounts ],
totalSparkStats =
mkSparkStats $
foldl' (binopSparks (+)) zeroSparks
[ sparkCounts | (_cap, sparkCounts) <- capsSparkCounts ]
}
where
capsSparkCounts =
[ (cap, sparkCounts)
| (cap, (countsEnd, countsStart)) <- IM.assocs dsparkTable
, let sparkCounts = binopSparks (-) countsEnd countsStart ]
mkSparkStats RtsSpark {sparkCreated, sparkDud, sparkOverflowed,
sparkConverted, sparkFizzled, sparkGCd} =
-- in our final presentation we show the total created,
-- and the breakdown of that into outcomes:
SparkCounts (sparkCreated + sparkDud + sparkOverflowed)
sparkConverted sparkOverflowed
sparkDud sparkGCd sparkFizzled
------------------------------------------------------------------------------
showTimeWithUnit :: Integral a => a -> String
showTimeWithUnit t =
showFFloat (Just 3) t'' unit
where
(t'', unit) =
case timeToSecondsDbl t of
t' | t' < 1e-6 -> (t' / 1e-9, "ns")
| t' < 1e-3 -> (t' / 1e-6, "μs")
| t' < 1 -> (t' / 1e-3, "ms")
| otherwise -> (t', "s")
timeToSecondsDbl :: Integral a => a -> Double
timeToSecondsDbl t = timeToSeconds $ fromIntegral t
timeToSeconds :: Double -> Double
timeToSeconds t = t / tIME_RESOLUTION
where tIME_RESOLUTION = 1000000
------------------------------------------------------------------------------
-- The single-pass stats accumulation stuff
--
-- | Data collected and computed gradually while events are scanned.
data StatsAccum = StatsAccum
{ dallocTable :: !(IM.IntMap (Word64, Word64)) -- indexed by caps
, dcopied :: !(Maybe Word64)
, dmaxResidency :: !(Maybe Word64)
, dmaxSlop :: !(Maybe Word64)
, dmaxMemory :: !(Maybe Word64)
--, dmaxFrag :: Maybe Word64 -- not important enough
, dGCTable :: !(IM.IntMap RtsGC) -- indexed by caps
-- Here we store the official +RTS -s timings of GCs,
-- that is times aggregated from the main caps of all GCs.
-- For now only gcElapsed and gcMaxPause are needed, so the rest
-- of the fields stays at default values.
, dGCMain :: !(Maybe RtsGC)
, dparMaxCopied :: !(Maybe Word64)
, dparTotCopied :: !(Maybe Word64)
, dmaxParNThreads :: !(Maybe Int)
--, dtaskTable -- of questionable usefulness, hard to get
, dsparkTable :: !(IM.IntMap (RtsSpark, RtsSpark)) -- indexed by caps
--, dInitExitT -- TODO. At least init time can be included in the total
-- time registered in the eventlog. Can we measure this
-- as the time between some initial events?
--, dGCTime -- Is better computed after all events are scanned,
-- e.g., because the same info can be used to calculate
-- per-cap GCTime and other per-cap stats.
--, dtotalTime -- TODO: can we measure this excluding INIT or EXIT times?
}
data RtsSpark = RtsSpark
{ sparkCreated, sparkDud, sparkOverflowed
, sparkConverted, sparkFizzled, sparkGCd :: !Word64
}
zeroSparks :: RtsSpark
zeroSparks = RtsSpark 0 0 0 0 0 0
binopSparks :: (Word64 -> Word64 -> Word64) -> RtsSpark -> RtsSpark -> RtsSpark
binopSparks op (RtsSpark crt1 dud1 ovf1 cnv1 fiz1 gcd1)
(RtsSpark crt2 dud2 ovf2 cnv2 fiz2 gcd2) =
RtsSpark (crt1 `op` crt2) (dud1 `op` dud2) (ovf1 `op` ovf2)
(cnv1 `op` cnv2) (fiz1 `op` fiz2) (gcd1 `op` gcd2)
type Gen = Int
type Cap = Int
data GcMode =
ModeInit | ModeStart | ModeSync Cap | ModeGHC Cap Gen | ModeEnd | ModeIdle
deriving Eq
data RtsGC = RtsGC
{ gcMode :: !GcMode
, gcStartTime :: !Timestamp
, gcGenStat :: !(IM.IntMap GenStat) -- indexed by generations
}
-- Index at the @gcGenStat@ map at which we store the sum of stats over all
-- generations, or the single set of stats for non-genenerational GC models.
gcGenTot :: Gen
gcGenTot = -1
data GenStat = GenStat
{ -- Sum over all seqential and pararell GC invocations.
gcAll :: !Int
, -- Only parallel GCs. For GC models without stop-the-world par, always 0.
gcPar :: !Int
, gcElapsed :: !Timestamp
, gcMaxPause :: !Timestamp
}
emptyStatsAccum :: StatsAccum
emptyStatsAccum = StatsAccum
{ dallocTable = IM.empty
, dcopied = Nothing
, dmaxResidency = Nothing
, dmaxSlop = Nothing
, dmaxMemory = Nothing
, dGCTable = IM.empty
, dGCMain = Nothing
, dparMaxCopied = Nothing
, dparTotCopied = Nothing
, dmaxParNThreads = Nothing
, dsparkTable = IM.empty
}
-- | At the beginning of a program run, we know for sure several of the
-- stats start at zero:
zeroStatsAccum :: StatsAccum
zeroStatsAccum = emptyStatsAccum {
dcopied = Just 0,
dmaxResidency = Just 0,
dmaxSlop = Just 0,
dmaxMemory = Just 0,
dallocTable = -- a hack: we assume no more than 999 caps
IM.fromDistinctAscList $ zip [0..999] $ repeat (0, 0)
-- FIXME: but also, we should have a way to init to 0 for all caps.
}
defaultGC :: Timestamp -> RtsGC
defaultGC time = RtsGC
{ gcMode = ModeInit
, gcStartTime = time
, gcGenStat = IM.empty
}
emptyGenStat :: GenStat
emptyGenStat = GenStat
{ gcAll = 0
, gcPar = 0
, gcElapsed = 0
, gcMaxPause = 0
}
-- Fail only when assertions are turned on.
errorAs :: String -> a -> a
errorAs msg a = assert (error msg) a
accumEvent :: StatsAccum -> Event -> StatsAccum
accumEvent !statsAccum ev =
let -- For events that contain a counter with a running sum.
-- Eventually we'll subtract the last found
-- event from the first. Intervals beginning at time 0
-- are a special case, because morally the first event should have
-- value 0, but it may be absent, so we start with @Just (0, 0)@.
alterCounter n Nothing = Just (n, n)
alterCounter n (Just (_previous, first)) = Just (n, first)
-- For events that contain discrete increments. We assume the event
-- is emitted close to the end of the process it measures,
-- so we ignore the first found event, because most of the process
-- could have happened before the start of the current interval.
-- This is consistent with @alterCounter@. For interval beginning
-- at time 0, we start with @Just 0@.
alterIncrement _ Nothing = Just 0
alterIncrement n (Just k) = Just (k + n)
-- For events that contain sampled values, where a max is sought.
alterMax n Nothing = Just n
alterMax n (Just k) | n > k = Just n
alterMax _ jk = jk
-- Scan events, updating summary data.
scan !sd@StatsAccum{..} Event{evTime, evSpec, evCap} =
let cap = fromMaybe (error "Error: missing cap; use 'ghc-events validate' to verify the eventlog") evCap
capGC = IM.findWithDefault (defaultGC evTime) cap dGCTable
in case evSpec of
HeapAllocated{allocBytes} ->
sd { dallocTable =
IM.alter (alterCounter allocBytes) cap dallocTable }
HeapLive{liveBytes} ->
sd { dmaxResidency = alterMax liveBytes dmaxResidency}
HeapSize{sizeBytes} ->
sd { dmaxMemory = alterMax sizeBytes dmaxMemory}
StartGC ->
assert (gcMode capGC `elem` [ModeInit, ModeEnd, ModeIdle]) $
let newGC = capGC { gcMode = ModeStart
, gcStartTime = evTime
}
-- TODO: Index with generations, not caps?
in sd { dGCTable = IM.insert cap newGC dGCTable }
GlobalSyncGC ->
-- All caps must be stopped. Those that take part in the GC
-- are in ModeInit or ModeStart, those that do not
-- are in ModeInit, ModeEnd or ModeIdle.
assert (L.all (notModeGHCEtc . gcMode) (IM.elems dGCTable)) $
sd { dGCTable = IM.mapWithKey setSync dGCTable }
where
notModeGHCEtc ModeGHC{} = False
notModeGHCEtc ModeSync{} = False
notModeGHCEtc _ = True
someInit = L.any ((== ModeInit) . gcMode) (IM.elems dGCTable)
setSync capKey dGC@RtsGC{gcGenStat}
| someInit =
-- If even one cap could possibly have started GC before
-- the start of the selected interval, skip the GC on all caps.
-- We don't verify the overwritten modes in this case.
-- TODO: we could be smarter and defer the decision to EndGC,
-- when we can deduce if the suspect caps take part in GC
-- or not at all.
dGC { gcMode = ModeInit }
| otherwise =
let totGC = IM.findWithDefault emptyGenStat gcGenTot gcGenStat
in case gcMode dGC of
-- Cap takes part in the GC (not known if seq or par).
-- Here is the moment where all caps taking place in the GC
-- are identified and we can aggregate all their data
-- at once (currently we just increment a counter for each).
-- The EndGC events can come much later for some caps and at
-- that time other caps are already inside their new GC.
ModeStart ->
dGC { gcMode = ModeSync cap
, gcGenStat =
if capKey == cap
then IM.insert gcGenTot
totGC{ gcAll = gcAll totGC + 1 }
gcGenStat
else gcGenStat
}
-- Cap is not in the GC. Mark it as idle to complete
-- the identification of caps that take part
-- in the current GC. Without overwriting the mode,
-- the cap could be processed later on as if
-- it took part in the GC, giving wrong results.
ModeEnd -> dGC { gcMode = ModeIdle }
ModeIdle -> dGC
-- Impossible.
ModeInit -> errorAs "scanEvents: GlobalSyncGC ModeInit" dGC
ModeSync{} -> errorAs "scanEvents: GlobalSyncGC ModeSync" dGC
ModeGHC{} -> -- error "scanEvents: GlobalSyncGC ModeGHC"
dGC -- workaround for #46
GCStatsGHC{..} ->
-- All caps must be stopped. Those that take part in the GC
-- are in ModeInit or ModeSync, those that do not
-- are in ModeInit or ModeIdle.
assert (L.all (notModeStartEtc . gcMode) (IM.elems dGCTable)) $
sd { dcopied = alterIncrement copied dcopied -- sum over caps
, dmaxSlop = alterMax slop dmaxSlop -- max over all caps
, dGCTable = IM.mapWithKey setParSeq dGCTable
, dparMaxCopied = alterIncrement parMaxCopied dparMaxCopied
, dparTotCopied = alterIncrement parTotCopied dparTotCopied
, dmaxParNThreads = alterMax parNThreads dmaxParNThreads
}
where
notModeStartEtc ModeStart = False
notModeStartEtc ModeGHC{} = False
notModeStartEtc ModeEnd = False
notModeStartEtc _ = True
someInit = L.any ((== ModeInit) . gcMode) (IM.elems dGCTable)
setParSeq capKey dGC@RtsGC{gcGenStat}
| someInit =
-- Just starting the selected interval, so skip the GC.
dGC
| otherwise =
let genGC = IM.findWithDefault emptyGenStat gen gcGenStat
totGC = IM.findWithDefault emptyGenStat gcGenTot gcGenStat
in case gcMode dGC of
-- Cap takes part in seq GC.
ModeSync capSync | parNThreads == 1 ->
assert (cap == capSync) $
dGC { gcMode = ModeGHC cap gen
, gcGenStat =
-- Already inserted into gcGenTot in GlobalSyncGC,
-- so only inserting into gen.
if capKey == cap
then IM.insert gen
genGC{ gcAll = gcAll genGC + 1 }
gcGenStat
else gcGenStat
}
-- Cap takes part in par GC.
ModeSync capSync ->
assert (cap == capSync) $
assert (parNThreads > 1) $
dGC { gcMode = ModeGHC cap gen
, gcGenStat =
if capKey == cap
then IM.insert gen
genGC{ gcAll = gcAll genGC + 1
, gcPar = gcPar genGC + 1
}
(IM.insert gcGenTot
-- Already incremented gcAll in SyncGC.
totGC{ gcPar = gcPar totGC + 1 }
gcGenStat)
else gcGenStat
}
-- Cap not in the current GC, leave it alone.
ModeIdle -> dGC
-- Impossible.
ModeInit -> errorAs "scanEvents: GCStatsGHC ModeInit" dGC
ModeGHC{} -> -- error "scanEvents: GCStatsGHC ModeGHC"
dGC -- workaround for #46
-- The last two cases are copied from case @GlobalSyncGC@
-- to work around low-resolution timestamps (#35).
-- Normally, these states would be impossible here, because
-- @GlobalSyncGC@ would already transition away from these
-- states. But if @GlobalSyncGC@ comes too early, the states
-- can appear here. The computed stats are usually only
-- slightly different than if @GlobalSyncGC@ made the state
-- transitions, because the timestamps of @GCStatsGHC@
-- and @GlobalSyncGC@ are normally only slightly different.
--
-- Cap takes part in the GC (not known if seq or par).
-- Here is the moment where all caps taking place in the GC
-- are identified and we can aggregate all their data
-- at once (currently we just increment a counter for each).
-- The EndGC events can come much later for some caps and at
-- that time other caps are already inside their new GC.
ModeStart ->
dGC { gcMode = ModeSync cap
, gcGenStat =
if capKey == cap
then IM.insert gcGenTot
totGC{ gcAll = gcAll totGC + 1 }
gcGenStat
else gcGenStat
}
-- Cap is not in the GC. Mark it as idle to complete
-- the identification of caps that take part
-- in the current GC. Without overwriting the mode,
-- the cap could be processed later on as if
-- it took part in the GC, giving wrong results.
ModeEnd -> dGC { gcMode = ModeIdle }
EndGC ->
assert (gcMode capGC `notElem` [ModeEnd, ModeIdle]) $
let endedGC = capGC { gcMode = ModeEnd }
duration = evTime - gcStartTime capGC
timeGC gen gstat =
let genGC =
IM.findWithDefault emptyGenStat gen (gcGenStat gstat)
newGenGC =
genGC { gcElapsed = gcElapsed genGC + duration
, gcMaxPause = max (gcMaxPause genGC) duration
}
in gstat { gcGenStat = IM.insert gen newGenGC
(gcGenStat gstat) }
timeGenTot = timeGC gcGenTot endedGC
updateMainCap mainCap _ dgm | mainCap /= cap = dgm
updateMainCap _ currentGen dgm =
-- We are at the EndGC event of the main cap of current GC.
-- The timings from this cap are the only that +RTS -s uses.
-- We will record them in the dGCMain field to be able
-- to display a look-alike of +RTS -s.
timeGC currentGen dgm
in case gcMode capGC of
-- We don't know the exact timing of this GC started before
-- the selected interval, so we skip it and clear its mode.
ModeInit -> sd { dGCTable = IM.insert cap endedGC dGCTable }
-- There is no GlobalSyncGC nor GCStatsGHC for this GC.
-- Consequently, we can't determine the main cap,
-- so skip it and and clear its mode.
ModeStart -> sd { dGCTable = IM.insert cap endedGC dGCTable }
-- There is no GCStatsGHC for this GC. Gather partial data.
ModeSync mainCap ->
let dgm = fromMaybe (defaultGC evTime) dGCMain
mainGenTot = updateMainCap mainCap gcGenTot dgm
in sd { dGCTable = IM.insert cap timeGenTot dGCTable
, dGCMain = Just mainGenTot
}
-- All is known, so we update the times.
ModeGHC mainCap gen ->
let newTime = timeGC gen timeGenTot
dgm = fromMaybe (defaultGC evTime) dGCMain
mainGenTot = updateMainCap mainCap gcGenTot dgm
newMain = updateMainCap mainCap gen mainGenTot
in sd { dGCTable = IM.insert cap newTime dGCTable
, dGCMain = Just newMain
}
ModeEnd -> errorAs "scanEvents: EndGC ModeEnd" sd
ModeIdle -> errorAs "scanEvents: EndGC ModeIdle"
$ sd { dGCTable = IM.insert cap endedGC dGCTable }
SparkCounters crt dud ovf cnv fiz gcd _rem ->
-- We are guaranteed the first spark counters event has all zeroes,
-- do we don't need to rig the counters for maximal interval.
let current = RtsSpark crt dud ovf cnv fiz gcd
in sd { dsparkTable =
IM.alter (alterCounter current) cap dsparkTable }
_ -> sd
in scan statsAccum ev
================================================
FILE: GUI/Timeline/Activity.hs
================================================
module GUI.Timeline.Activity (
renderActivity
) where
import GUI.Timeline.Render.Constants
import Events.HECs
import Events.EventTree
import Events.EventDuration
import GUI.Types
import GUI.ViewerColours
import Graphics.Rendering.Cairo
import Control.Monad
import Data.List
-- ToDo:
-- - we average over the slice, but the point is drawn at the beginning
-- of the slice rather than in the middle.
-----------------------------------------------------------------------------
renderActivity :: ViewParameters -> HECs -> Timestamp -> Timestamp
-> Render ()
renderActivity ViewParameters{..} hecs start0 end0 = do
let
slice = ceiling (fromIntegral activity_detail * scaleValue)
-- round the start time down, and the end time up, to a slice boundary
start = (start0 `div` slice) * slice
end = ((end0 + slice) `div` slice) * slice
hec_profs = map (actProfile slice start end)
(map (\ (t, _, _) -> t) (hecTrees hecs))
total_prof = map sum (transpose hec_profs)
-- liftIO $ printf "%s\n" (show (map length hec_profs))
-- liftIO $ printf "%s\n" (show (map (take 20) hec_profs))
drawActivity hecs start end slice total_prof
(if not bwMode then runningColour else black)
activity_detail :: Int
activity_detail = 4 -- in pixels
-- for each timeslice, the amount of time spent in the mutator
-- during that period.
actProfile :: Timestamp -> Timestamp -> Timestamp -> DurationTree -> [Timestamp]
actProfile slice start0 end0 t
= {- trace (show flat) $ -} chopped
where
-- do an extra slice at both ends
start = if start0 < slice then start0 else start0 - slice
end = end0 + slice
flat = flatten start t []
chopped0 = chop 0 start flat
chopped | start0 < slice = 0 : chopped0
| otherwise = chopped0
flatten :: Timestamp -> DurationTree -> [DurationTree] -> [DurationTree]
flatten _start DurationTreeEmpty rest = rest
flatten start t@(DurationSplit s split e l r _run _) rest
| e <= start = rest
| end <= s = rest
| start >= split = flatten start r rest
| end <= split = flatten start l rest
| e - s > slice = flatten start l $ flatten start r rest
| otherwise = t : rest
flatten _start t@(DurationTreeLeaf _) rest
= t : rest
chop :: Timestamp -> Timestamp -> [DurationTree] -> [Timestamp]
chop sofar start _ts
| start >= end = if sofar > 0 then [sofar] else []
chop sofar start []
= sofar : chop 0 (start+slice) []
chop sofar start (t : ts)
| e <= start
= if sofar /= 0
then error "chop"
else chop sofar start ts
| s >= start + slice
= sofar : chop 0 (start + slice) (t : ts)
| e > start + slice
= (sofar + time_in_this_slice t) : chop 0 (start + slice) (t : ts)
| otherwise
= chop (sofar + time_in_this_slice t) start ts
where
(s, e)
| DurationTreeLeaf ev <- t = (startTimeOf ev, endTimeOf ev)
| DurationSplit s _ e _ _ _run _ <- t = (s, e)
mi = min (start + slice) e
ma = max start s
duration = if mi < ma then 0 else mi - ma
time_in_this_slice t = case t of
DurationTreeLeaf ThreadRun{} -> duration
DurationTreeLeaf _ -> 0
DurationSplit _ _ _ _ _ run _ ->
round (fromIntegral (run * duration) / fromIntegral (e-s))
DurationTreeEmpty -> error "time_in_this_slice"
drawActivity :: HECs -> Timestamp -> Timestamp -> Timestamp -> [Timestamp]
-> Color
-> Render ()
drawActivity hecs start end slice ts color = do
case ts of
[] -> return ()
t:ts -> do
-- liftIO $ printf "ts: %s\n" (show (t:ts))
-- liftIO $ printf "off: %s\n" (show (map off (t:ts) :: [Double]))
let dstart = fromIntegral start
dend = fromIntegral end
dslice = fromIntegral slice
dheight = fromIntegral activityGraphHeight
-- funky gradients don't seem to work:
-- withLinearPattern 0 0 0 dheight $ \pattern -> do
-- patternAddColorStopRGB pattern 0 0.8 0.8 0.8
-- patternAddColorStopRGB pattern 1.0 1.0 1.0 1.0
-- rectangle dstart 0 dend dheight
-- setSource pattern
-- fill
newPath
moveTo (dstart-dslice/2) (off t)
zipWithM_ lineTo (tail [dstart-dslice/2, dstart+dslice/2 ..]) (map off ts)
setSourceRGBAhex black 1.0
setLineWidth 1
strokePreserve
lineTo dend dheight
lineTo dstart dheight
setSourceRGBAhex color 1.0
fill
-- funky gradients don't seem to work:
-- save
-- withLinearPattern 0 0 0 dheight $ \pattern -> do
-- patternAddColorStopRGB pattern 0 0 1.0 0
-- patternAddColorStopRGB pattern 1.0 1.0 1.0 1.0
-- setSource pattern
-- -- identityMatrix
-- -- setFillRule FillRuleEvenOdd
-- fillPreserve
-- restore
save
forM_ [0 .. hecCount hecs - 1] $ \h -> do
let y = fromIntegral (floor (fromIntegral h * dheight / fromIntegral (hecCount hecs))) - 0.5
setSourceRGBAhex black 0.3
moveTo dstart y
lineTo dend y
dashedLine1
restore
where
off t = fromIntegral activityGraphHeight -
fromIntegral (t * fromIntegral activityGraphHeight) /
fromIntegral (fromIntegral (hecCount hecs) * slice)
-- | Draw a dashed line along the current path.
dashedLine1 :: Render ()
dashedLine1 = do
save
identityMatrix
let dash = fromIntegral ox
setDash [dash, dash] 0.0
setLineWidth 1
stroke
restore
================================================
FILE: GUI/Timeline/CairoDrawing.hs
================================================
-------------------------------------------------------------------------------
--- $Id: CairoDrawing.hs#3 2009/07/18 22:48:30 REDMOND\\satnams $
--- $Source: //depot/satnams/haskell/ThreadScope/CairoDrawing.hs $
-------------------------------------------------------------------------------
module GUI.Timeline.CairoDrawing
where
import Graphics.Rendering.Cairo
import qualified Graphics.Rendering.Cairo as C
import Control.Monad
-------------------------------------------------------------------------------
{-# INLINE draw_line #-}
draw_line :: (Integral a, Integral b, Integral c, Integral d) =>
(a, b) -> (c, d) -> Render ()
draw_line (x0, y0) (x1, y1)
= do move_to (x0, y0)
lineTo (fromIntegral x1) (fromIntegral y1)
stroke
{-# INLINE move_to #-}
move_to :: (Integral a, Integral b) => (a, b) -> Render ()
move_to (x, y)
= moveTo (fromIntegral x) (fromIntegral y)
{-# INLINE rel_line_to #-}
rel_line_to :: (Integral a, Integral b) => (a, b) -> Render ()
rel_line_to (x, y)
= relLineTo (fromIntegral x) (fromIntegral y)
-------------------------------------------------------------------------------
{-# INLINE draw_rectangle #-}
draw_rectangle :: (Integral x, Integral y, Integral w, Integral h)
=> x -> y -> w -> h
-> Render ()
draw_rectangle x y w h = do
rectangle (fromIntegral x) (fromIntegral y) (fromIntegral w) (fromIntegral h)
C.fill
-------------------------------------------------------------------------------
{-# INLINE draw_outlined_rectangle #-}
draw_outlined_rectangle :: (Integral x, Integral y, Integral w, Integral h)
=> x -> y -> w -> h
-> Render ()
draw_outlined_rectangle x y w h = do
rectangle (fromIntegral x) (fromIntegral y) (fromIntegral w) (fromIntegral h)
fillPreserve
setLineWidth 1
setSourceRGBA 0 0 0 0.7
stroke
-------------------------------------------------------------------------------
{-# INLINE draw_rectangle_opt #-}
draw_rectangle_opt :: (Integral x, Integral y, Integral w, Integral h)
=> Bool -> x -> y -> w -> h
-> Render ()
draw_rectangle_opt opt x y w h
= draw_rectangle_opt' opt (fromIntegral x) (fromIntegral y)
(fromIntegral w) (fromIntegral h)
draw_rectangle_opt' :: Bool -> Double -> Double -> Double -> Double
-> Render ()
draw_rectangle_opt' opt x y w h
= do rectangle x y (1.0 `max` w) h
C.fill
when opt $ do
setLineWidth 1
setSourceRGBA 0 0 0 0.7
rectangle x y w h
stroke
-------------------------------------------------------------------------------
{-# INLINE draw_rectangle_outline #-}
draw_rectangle_outline :: (Integral x, Integral y, Integral w, Integral h)
=> x -> y -> w -> h
-> Render ()
draw_rectangle_outline x y w h = do
setLineWidth 2
rectangle (fromIntegral x) (fromIntegral y) (fromIntegral w) (fromIntegral h)
stroke
-------------------------------------------------------------------------------
clearWhite :: Render ()
clearWhite = do
save
setOperator OperatorSource
setSourceRGBA 0xffff 0xffff 0xffff 0xffff
paint
restore
================================================
FILE: GUI/Timeline/HEC.hs
================================================
{-# LANGUAGE OverloadedStrings #-}
module GUI.Timeline.HEC (
renderHEC,
renderInstantHEC,
) where
import GUI.Timeline.Render.Constants
import Events.EventDuration
import Events.EventTree
import GUI.Timeline.CairoDrawing
import GUI.Types
import GUI.ViewerColours
import Graphics.Rendering.Cairo
import GHC.RTS.Events hiding (Event, GCIdle, GCWork)
import qualified GHC.RTS.Events as GHC
import Control.Monad
import qualified Data.IntMap as IM
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.Text.Lazy.Builder.Int as TB (decimal)
import Prelude
renderHEC :: ViewParameters -> Timestamp -> Timestamp
-> IM.IntMap Text -> (DurationTree,EventTree)
-> Render ()
renderHEC params@ViewParameters{..} start end perfNames (dtree,etree) = do
renderDurations params start end dtree
when (scaleValue < detailThreshold) $
case etree of
EventTree ltime etime tree -> do
renderEvents params ltime etime start end (fromIntegral detail)
perfNames tree
return ()
renderInstantHEC :: ViewParameters -> Timestamp -> Timestamp
-> IM.IntMap Text -> EventTree
-> Render ()
renderInstantHEC params start end
perfNames (EventTree ltime etime tree) = do
let instantDetail = 1
renderEvents params ltime etime start end instantDetail perfNames tree
return ()
detailThreshold :: Double
detailThreshold = 3
-------------------------------------------------------------------------------
-- draws the trace for a single HEC
renderDurations :: ViewParameters
-> Timestamp -> Timestamp -> DurationTree
-> Render ()
renderDurations _ _ _ DurationTreeEmpty = return ()
renderDurations params startPos endPos (DurationTreeLeaf e)
| inView startPos endPos e = drawDuration params e
| otherwise = return ()
renderDurations params@ViewParameters{..} !startPos !endPos
(DurationSplit s splitTime e lhs rhs runAv gcAv)
| startPos < splitTime && endPos >= splitTime &&
(fromIntegral (e - s) / scaleValue) <= fromIntegral detail
= -- View spans both left and right sub-tree.
-- trace (printf "renderDurations (average): start:%d end:%d s:%d e:%d" startPos endPos s e) $
drawAverageDuration params s e runAv gcAv
| otherwise
= -- trace (printf "renderDurations: start:%d end:%d s:%d e:%d" startPos endPos s e) $
do when (startPos < splitTime) $
renderDurations params startPos endPos lhs
when (endPos >= splitTime) $
renderDurations params startPos endPos rhs
-------------------------------------------------------------------------------
renderEvents :: ViewParameters
-> Timestamp -- start time of this tree node
-> Timestamp -- end time of this tree node
-> Timestamp -> Timestamp -> Double
-> IM.IntMap Text -> EventNode
-> Render Bool
renderEvents params !_s !_e !startPos !endPos ewidth
perfNames (EventTreeLeaf es)
= let within = [ e | e <- es, let t = evTime e, t >= startPos && t < endPos ]
untilTrue _ [] = return False
untilTrue f (x : xs) = do
b <- f x
if b then return b else untilTrue f xs
in untilTrue (drawEvent params ewidth perfNames) within
renderEvents params !_s !_e !startPos !endPos ewidth
perfNames (EventTreeOne ev)
| t >= startPos && t < endPos = drawEvent params ewidth perfNames ev
| otherwise = return False
where t = evTime ev
renderEvents params@ViewParameters{..} !s !e !startPos !endPos ewidth
perfNames (EventSplit splitTime lhs rhs)
| startPos < splitTime && endPos >= splitTime &&
(fromIntegral (e - s) / scaleValue) <= ewidth
= do drawnLhs <-
renderEvents params s splitTime startPos endPos ewidth perfNames lhs
if not drawnLhs
then
renderEvents params splitTime e startPos endPos ewidth perfNames rhs
else return True
| otherwise
= do drawnLhs <-
if startPos < splitTime
then
renderEvents params s splitTime startPos endPos ewidth perfNames lhs
else return False
drawnRhs <-
if endPos >= splitTime
then
renderEvents params splitTime e startPos endPos ewidth perfNames rhs
else return False
return $ drawnLhs || drawnRhs
-------------------------------------------------------------------------------
-- An event is in view if it is not outside the view.
inView :: Timestamp -> Timestamp -> EventDuration -> Bool
inView viewStart viewEnd event =
not (eStart > viewEnd || eEnd <= viewStart)
where
eStart = startTimeOf event
eEnd = endTimeOf event
-------------------------------------------------------------------------------
drawAverageDuration :: ViewParameters
-> Timestamp -> Timestamp -> Timestamp -> Timestamp
-> Render ()
drawAverageDuration ViewParameters{..} startTime endTime runAv gcAv = do
setSourceRGBAhex (if not bwMode then runningColour else black) 1.0
when (runAv > 0) $
draw_rectangle startTime hecBarOff -- x, y
(endTime - startTime) -- w
hecBarHeight
setSourceRGBAhex black 1.0
--move_to (oxs + startTime, 0)
--relMoveTo (4/scaleValue) 13
--unscaledText scaleValue (show nrEvents)
setSourceRGBAhex (if not bwMode then gcColour else black) gcRatio
draw_rectangle startTime -- x
(hecBarOff+hecBarHeight) -- y
(endTime - startTime) -- w
(hecBarHeight `div` 2) -- h
where
duration = endTime - startTime
-- runRatio :: Double
-- runRatio = (fromIntegral runAv) / (fromIntegral duration)
gcRatio :: Double
gcRatio = (fromIntegral gcAv) / (fromIntegral duration)
-------------------------------------------------------------------------------
unscaledText :: String -> Render ()
unscaledText text
= do m <- getMatrix
identityMatrix
showText text
setMatrix m
-------------------------------------------------------------------------------
textWidth :: Double -> String -> Render TextExtents
textWidth _scaleValue text
= do m <- getMatrix
identityMatrix
tExtent <- textExtents text
setMatrix m
return tExtent
-------------------------------------------------------------------------------
drawDuration :: ViewParameters -> EventDuration -> Render ()
drawDuration ViewParameters{..} (ThreadRun t s startTime endTime) = do
setSourceRGBAhex (if not bwMode then runningColour else black) 1.0
setLineWidth (1/scaleValue)
draw_rectangle_opt False
startTime -- x
hecBarOff -- y
(endTime - startTime) -- w
hecBarHeight -- h
-- Optionally label the bar with the threadID if there is room
tExtent <- textWidth scaleValue tStr
let tw = textExtentsWidth tExtent
th = textExtentsHeight tExtent
when (tw + 6 < fromIntegral rectWidth) $ do
setSourceRGBAhex labelTextColour 1.0
move_to (fromIntegral startTime + truncate (4*scaleValue),
hecBarOff + (hecBarHeight + round th) `quot` 2)
unscaledText tStr
-- Optionally write the reason for the thread being stopped
-- depending on the zoom value
labelAt labelsMode endTime $
T.pack $ show t ++ " " ++ showThreadStopStatus s
where
rectWidth = truncate (fromIntegral (endTime - startTime) / scaleValue) -- as pixels
tStr = show t
drawDuration ViewParameters{..} (GCStart startTime endTime)
= gcBar (if bwMode then black else gcStartColour) startTime endTime
drawDuration ViewParameters{..} (GCWork startTime endTime)
= gcBar (if bwMode then black else gcWorkColour) startTime endTime
drawDuration ViewParameters{..} (GCIdle startTime endTime)
= gcBar (if bwMode then black else gcIdleColour) startTime endTime
drawDuration ViewParameters{..} (GCEnd startTime endTime)
= gcBar (if bwMode then black else gcEndColour) startTime endTime
gcBar :: Color -> Timestamp -> Timestamp -> Render ()
gcBar col !startTime !endTime = do
setSourceRGBAhex col 1.0
draw_rectangle_opt False
startTime -- x
(hecBarOff+hecBarHeight) -- y
(endTime - startTime) -- w
(hecBarHeight `div` 2) -- h
labelAt :: Bool -> Timestamp -> Text -> Render ()
labelAt labelsMode t str
| not labelsMode = return ()
| otherwise = do
setSourceRGB 0.0 0.0 0.0
move_to (t, hecBarOff+hecBarHeight+12)
save
identityMatrix
rotate (pi/4)
showText str
restore
drawEvent :: ViewParameters -> Double -> IM.IntMap Text -> GHC.Event
-> Render Bool
drawEvent params ewidth perfNames event =
let renderI = renderInstantEvent params perfNames event ewidth
in case evSpec event of
CreateThread{} -> renderI createThreadColour
RequestSeqGC{} -> renderI seqGCReqColour
RequestParGC{} -> renderI parGCReqColour
MigrateThread{} -> renderI migrateThreadColour
WakeupThread{} -> renderI threadWakeupColour
Shutdown{} -> renderI shutdownColour
SparkCreate{} -> renderI createdConvertedColour
SparkDud{} -> renderI fizzledDudsColour
SparkOverflow{} -> renderI overflowedColour
SparkRun{} -> renderI createdConvertedColour
SparkSteal{} -> renderI createdConvertedColour
SparkFizzle{} -> renderI fizzledDudsColour
SparkGC{} -> renderI gcColour
UserMessage{} -> renderI userMessageColour
PerfCounter{} -> renderI createdConvertedColour
PerfTracepoint{} -> renderI shutdownColour
PerfName{} -> return False
RunThread{} -> return False
StopThread{} -> return False
StartGC{} -> return False
_ -> return False
renderInstantEv
gitextract_n4vjqfzb/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Events/ │ ├── EventDuration.hs │ ├── EventTree.hs │ ├── HECs.hs │ ├── ReadEvents.hs │ ├── SparkStats.hs │ ├── SparkTree.hs │ └── TestEvents.hs ├── GUI/ │ ├── App.hs │ ├── BookmarkView.hs │ ├── ConcurrencyControl.hs │ ├── DataFiles.hs │ ├── Dialogs.hs │ ├── EventsView.hs │ ├── GtkExtras.hs │ ├── Histogram.hs │ ├── KeyView.hs │ ├── Main.hs │ ├── MainWindow.hs │ ├── ProgressView.hs │ ├── SaveAs.hs │ ├── StartupInfoView.hs │ ├── SummaryView.hs │ ├── Timeline/ │ │ ├── Activity.hs │ │ ├── CairoDrawing.hs │ │ ├── HEC.hs │ │ ├── Motion.hs │ │ ├── Render/ │ │ │ └── Constants.hs │ │ ├── Render.hs │ │ ├── Sparks.hs │ │ ├── Ticks.hs │ │ └── Types.hs │ ├── Timeline.hs │ ├── TraceView.hs │ ├── Types.hs │ └── ViewerColours.hs ├── Graphics/ │ └── UI/ │ └── Gtk/ │ └── ModelView/ │ └── TreeView/ │ └── Compat.hs ├── LICENSE ├── Main.hs ├── Makefile ├── README.md ├── Setup.hs ├── TODO ├── cabal.project ├── cabal.project.osx ├── include/ │ └── windows_cconv.h ├── index.html ├── papers/ │ └── haskell_symposium_2009/ │ ├── Makefile │ ├── bsort/ │ │ ├── BSort.hs │ │ ├── BSortPar.hs │ │ ├── BSortPar2.hs │ │ ├── BSortStreaming.hs │ │ └── Makefile │ ├── bsort.tex │ ├── fib/ │ │ ├── Fib1.hs │ │ ├── Fib2.hs │ │ └── Makefile │ ├── ghc-parallel-tuning.bib │ ├── ghc-parallel-tuning.tex │ ├── infrastructure.tex │ ├── motivation.tex │ ├── related-work.tex │ ├── sigplanconf.cls │ ├── sumEuler/ │ │ ├── Makefile │ │ ├── SumEuler0.hs │ │ ├── SumEuler1.hs │ │ ├── SumEuler2.hs │ │ └── SumEuler3.hs │ └── threadring.tex ├── scripts/ │ └── install-on-osx.sh ├── stack.osx.yaml ├── stack.yaml ├── tests/ │ ├── Hello.hs │ ├── Makefile │ ├── Null.hs │ ├── ParFib.hs │ └── SumEulerPar1.hs ├── threadscope.cabal └── threadscope.ui
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (584K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 5421,
"preview": "name: CI\non:\n push:\n branches:\n - master\n tags:\n - v*\n pull_request:\n release:\n\nenv:\n GHC_FOR_RELEAS"
},
{
"path": ".gitignore",
"chars": 36,
"preview": "dist-newstyle\ncabal.project.local~*\n"
},
{
"path": "CHANGELOG.md",
"chars": 2725,
"preview": "# Revision history for threadscope\n\n## 2025-05-29 - v0.2.15.0\n* Switch to GTK3 ([#137](https://github.com/haskell/Thread"
},
{
"path": "Events/EventDuration.hs",
"chars": 6248,
"preview": "-- This module supports a duration-based data-type to represent thread\n-- execution and GC information.\n\nmodule Events.E"
},
{
"path": "Events/EventTree.hs",
"chars": 9589,
"preview": "module Events.EventTree (\n DurationTree(..),\n mkDurationTree,\n\n runTimeOf, gcTimeOf,\n reportDurationTree"
},
{
"path": "Events/HECs.hs",
"chars": 2723,
"preview": "{-# LANGUAGE CPP #-}\nmodule Events.HECs (\n HECs(..),\n Event,\n Timestamp,\n\n eventIndexToTimestamp,\n timest"
},
{
"path": "Events/ReadEvents.hs",
"chars": 10320,
"preview": "module Events.ReadEvents (\n registerEventsFromFile, registerEventsFromTrace\n ) where\n\nimport Events.EventDuration\nim"
},
{
"path": "Events/SparkStats.hs",
"chars": 4310,
"preview": "module Events.SparkStats\n ( SparkStats(..)\n , initial, create, rescale, aggregate, agEx\n ) where\n\nimport Data.Word (W"
},
{
"path": "Events/SparkTree.hs",
"chars": 10342,
"preview": "module Events.SparkTree (\n SparkTree,\n sparkTreeMaxDepth,\n emptySparkTree,\n eventsToSparkDurations,\n mkSparkTree,\n "
},
{
"path": "Events/TestEvents.hs",
"chars": 10085,
"preview": "{-# LANGUAGE OverloadedStrings #-}\nmodule Events.TestEvents (testTrace)\nwhere\n\nimport Data.Word\nimport GHC.RTS.Events\n\n-"
},
{
"path": "GUI/App.hs",
"chars": 473,
"preview": "-------------------------------------------------------------------------------\n-- | Module : GUI.App\n--\n-- Platform-spe"
},
{
"path": "GUI/BookmarkView.hs",
"chars": 4485,
"preview": "module GUI.BookmarkView (\n BookmarkView,\n bookmarkViewNew,\n BookmarkViewActions(..),\n\n bookmarkViewGet,\n "
},
{
"path": "GUI/ConcurrencyControl.hs",
"chars": 2200,
"preview": "\nmodule GUI.ConcurrencyControl (\n ConcurrencyControl,\n start,\n fullSpeed,\n ) where\n\nimport qualified System.Gl"
},
{
"path": "GUI/DataFiles.hs",
"chars": 1171,
"preview": "{-# LANGUAGE ScopedTypeVariables #-}\n{-# LANGUAGE TemplateHaskell #-}\nmodule GUI.DataFiles\n ( ui\n , loadLogo\n ) where"
},
{
"path": "GUI/Dialogs.hs",
"chars": 5763,
"preview": "{-# LANGUAGE TemplateHaskell #-}\nmodule GUI.Dialogs where\n\nimport GUI.DataFiles (loadLogo)\nimport Paths_threadscope (ver"
},
{
"path": "GUI/EventsView.hs",
"chars": 12609,
"preview": "{-# LANGUAGE CPP #-}\n{-# LANGUAGE OverloadedStrings #-}\nmodule GUI.EventsView (\n EventsView,\n eventsViewNew,\n E"
},
{
"path": "GUI/GtkExtras.hs",
"chars": 2143,
"preview": "{-# LANGUAGE ForeignFunctionInterface, CPP #-}\nmodule GUI.GtkExtras where\n\n-- This is all stuff that should be bound in "
},
{
"path": "GUI/Histogram.hs",
"chars": 4668,
"preview": "{-# LANGUAGE ScopedTypeVariables #-}\n module GUI.Histogram (\n HistogramView,\n histogramViewNew,\n histogramView"
},
{
"path": "GUI/KeyView.hs",
"chars": 7014,
"preview": "module GUI.KeyView (\n KeyView,\n keyViewNew,\n ) where\n\nimport GUI.ViewerColours\nimport GUI.Timeline.Render.Constan"
},
{
"path": "GUI/Main.hs",
"chars": 15957,
"preview": "{-# LANGUAGE CPP #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE OverloadedStrings #-}\nmodule GUI.Main (runGUI) where\n"
},
{
"path": "GUI/MainWindow.hs",
"chars": 7345,
"preview": "{-# LANGUAGE TemplateHaskell #-}\nmodule GUI.MainWindow (\n MainWindow,\n mainWindowNew,\n MainWindowActions(..),\n\n"
},
{
"path": "GUI/ProgressView.hs",
"chars": 3405,
"preview": "{-# LANGUAGE DeriveDataTypeable #-}\n\nmodule GUI.ProgressView (\n ProgressView,\n withProgress,\n setText,\n setT"
},
{
"path": "GUI/SaveAs.hs",
"chars": 2829,
"preview": "module GUI.SaveAs (saveAsPDF, saveAsPNG) where\n\n-- Imports for ThreadScope\nimport GUI.Timeline.Render (renderTraces, ren"
},
{
"path": "GUI/StartupInfoView.hs",
"chars": 5594,
"preview": "{-# LANGUAGE OverloadedStrings #-}\n{-# LANGUAGE ViewPatterns #-}\nmodule GUI.StartupInfoView (\n StartupInfoView,\n s"
},
{
"path": "GUI/SummaryView.hs",
"chars": 40085,
"preview": "module GUI.SummaryView (\n SummaryView,\n summaryViewNew,\n summaryViewSetEvents,\n summaryViewSetInterval,\n ) "
},
{
"path": "GUI/Timeline/Activity.hs",
"chars": 5603,
"preview": "module GUI.Timeline.Activity (\n renderActivity\n ) where\n\nimport GUI.Timeline.Render.Constants\n\nimport Events.HECs\n"
},
{
"path": "GUI/Timeline/CairoDrawing.hs",
"chars": 3250,
"preview": "-------------------------------------------------------------------------------\n--- $Id: CairoDrawing.hs#3 2009/07/18 22"
},
{
"path": "GUI/Timeline/HEC.hs",
"chars": 11301,
"preview": "{-# LANGUAGE OverloadedStrings #-}\nmodule GUI.Timeline.HEC (\n renderHEC,\n renderInstantHEC,\n ) where\n\nimport GUI."
},
{
"path": "GUI/Timeline/Motion.hs",
"chars": 5212,
"preview": "module GUI.Timeline.Motion (\n zoomIn, zoomOut, zoomToFit,\n scrollLeft, scrollRight, scrollToBeginning, scrollToEnd"
},
{
"path": "GUI/Timeline/Render/Constants.hs",
"chars": 1415,
"preview": "module GUI.Timeline.Render.Constants (\n ox, firstTraceY, tracePad,\n hecTraceHeight, hecInstantHeight, hecSparksHei"
},
{
"path": "GUI/Timeline/Render.hs",
"chars": 17298,
"preview": "{-# LANGUAGE CPP #-}\nmodule GUI.Timeline.Render (\n renderView,\n renderTraces,\n updateXScaleArea,\n renderYSca"
},
{
"path": "GUI/Timeline/Sparks.hs",
"chars": 9512,
"preview": "module GUI.Timeline.Sparks (\n treesProfile,\n maxSparkRenderedValue,\n renderSparkCreation,\n renderSparkConver"
},
{
"path": "GUI/Timeline/Ticks.hs",
"chars": 11603,
"preview": "{-# LANGUAGE CPP #-}\nmodule GUI.Timeline.Ticks (\n renderVRulers,\n XScaleMode(..),\n renderXScaleArea,\n render"
},
{
"path": "GUI/Timeline/Types.hs",
"chars": 1171,
"preview": "module GUI.Timeline.Types (\n TimelineState(..),\n TimeSelection(..),\n ) where\n\n\nimport GUI.Types\n\nimport Graphics.U"
},
{
"path": "GUI/Timeline.hs",
"chars": 20554,
"preview": "{-# LANGUAGE CPP #-}\nmodule GUI.Timeline (\n TimelineView,\n timelineViewNew,\n TimelineViewActions(..),\n\n time"
},
{
"path": "GUI/TraceView.hs",
"chars": 7365,
"preview": "module GUI.TraceView (\n TraceView,\n traceViewNew,\n TraceViewActions(..),\n traceViewSetHECs,\n traceViewGet"
},
{
"path": "GUI/Types.hs",
"chars": 953,
"preview": "module GUI.Types (\n ViewParameters(..),\n Trace(..),\n Timestamp,\n Interval,\n ) where\n\nimport GHC.RTS.Events\n"
},
{
"path": "GUI/ViewerColours.hs",
"chars": 3545,
"preview": "-------------------------------------------------------------------------------\n--- $Id: ViewerColours.hs#2 2009/07/18 2"
},
{
"path": "Graphics/UI/Gtk/ModelView/TreeView/Compat.hs",
"chars": 660,
"preview": "{-# LANGUAGE CPP #-}\nmodule Graphics.UI.Gtk.ModelView.TreeView.Compat\n ( treeViewSetModel\n ) where\nimport Graphics"
},
{
"path": "LICENSE",
"chars": 1623,
"preview": "The Glasgow Haskell Compiler License\n\nCopyright 2002-2012, The University Court of the University of Glasgow\nand others."
},
{
"path": "Main.hs",
"chars": 2387,
"preview": "module Main where\n\nimport GUI.Main (runGUI)\n\nimport System.Environment\nimport System.Exit\nimport System.Console.GetOpt\ni"
},
{
"path": "Makefile",
"chars": 223,
"preview": "# Makefile for ThreadScope\r\n\r\nGHC = c:/ghc/ghc-6.10.3/bin/ghc\r\n\r\ncabal:\r\n\tcabal install -w $(GHC) --user --prefix=$(HOME"
},
{
"path": "README.md",
"chars": 3126,
"preview": "# ThreadScope\n\n[](https://hackage.haskell.org/package/thread"
},
{
"path": "Setup.hs",
"chars": 45,
"preview": "import Distribution.Simple\nmain = defaultMain"
},
{
"path": "TODO",
"chars": 19918,
"preview": "BUGS:\n\n- ThreadScope DEADLOCKs occasionally, more often with --debug, why?\n\n- X Window System error sometimes?\n\n- backgr"
},
{
"path": "cabal.project",
"chars": 109,
"preview": "-- see http://cabal.readthedocs.io/en/latest/nix-local-build-overview.html for more information\n\npackages: .\n"
},
{
"path": "cabal.project.osx",
"chars": 46,
"preview": "packages: .\nconstraints: gtk +have-quartz-gtk\n"
},
{
"path": "include/windows_cconv.h",
"chars": 223,
"preview": "#ifndef __WINDOWS_CCONV_H\n#define __WINDOWS_CCONV_H\n\n#if defined(i386_HOST_ARCH)\n# define WINDOWS_CCONV stdcall\n#elif de"
},
{
"path": "index.html",
"chars": 1116,
"preview": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
},
{
"path": "papers/haskell_symposium_2009/Makefile",
"chars": 466,
"preview": "# $Id: Makefile#3 2009/07/18 22:48:30 REDMOND\\\\satnams $\r\n# $Source: //depot/satnams/haskell/ThreadScope/papers/haskell_"
},
{
"path": "papers/haskell_symposium_2009/bsort/BSort.hs",
"chars": 6086,
"preview": "-------------------------------------------------------------------------------\r\n--- $Id: BSort.hs#1 2009/03/06 10:53:15"
},
{
"path": "papers/haskell_symposium_2009/bsort/BSortPar.hs",
"chars": 6142,
"preview": "-------------------------------------------------------------------------------\r\n--- $Id: BSort.hs#1 2009/03/06 10:53:15"
},
{
"path": "papers/haskell_symposium_2009/bsort/BSortPar2.hs",
"chars": 6187,
"preview": "-------------------------------------------------------------------------------\r\n--- $Id: BSort.hs#1 2009/03/06 10:53:15"
},
{
"path": "papers/haskell_symposium_2009/bsort/BSortStreaming.hs",
"chars": 6896,
"preview": "-------------------------------------------------------------------------------\r\n--- $Id: BSortStreaming.hs#1 2009/03/06"
},
{
"path": "papers/haskell_symposium_2009/bsort/Makefile",
"chars": 1439,
"preview": "GHC = /c/ghc/ghc/inplace/bin/ghc-stage2\r\nGHC_OPTS = -threaded -eventlog\r\n# HEAP = -H100M\r\nHEAP =\r\nEBH = -feager-blackhol"
},
{
"path": "papers/haskell_symposium_2009/bsort.tex",
"chars": 5781,
"preview": "\\subsection{Batcher's Bitonic Parallel Sorter}\r\nBatcher's bitonic merger and sorter is a parallel sorting algorithm whic"
},
{
"path": "papers/haskell_symposium_2009/fib/Fib1.hs",
"chars": 1298,
"preview": "-------------------------------------------------------------------------------\r\n\r\nmodule Main\r\nwhere\r\nimport System.Tim"
},
{
"path": "papers/haskell_symposium_2009/fib/Fib2.hs",
"chars": 1307,
"preview": "-------------------------------------------------------------------------------\r\n\r\nmodule Main\r\nwhere\r\nimport System.Tim"
},
{
"path": "papers/haskell_symposium_2009/fib/Makefile",
"chars": 323,
"preview": "GHC = /c/ghc/ghc/inplace/bin/ghc-stage2\r\nGHC_OPTS = -threaded -eventlog\r\n\r\nall:\t\r\n\t$(GHC) $(GHC_OPTS) --make Fib1.hs\r\n\t$"
},
{
"path": "papers/haskell_symposium_2009/ghc-parallel-tuning.bib",
"chars": 4639,
"preview": "% $Id: ghc-parallel-tuning.bib#3 2009/07/18 22:48:30 REDMOND\\\\satnams $\r\n% $Source: //depot/satnams/haskell/ThreadScope/"
},
{
"path": "papers/haskell_symposium_2009/ghc-parallel-tuning.tex",
"chars": 25778,
"preview": "\\documentclass[twocolumn,9pt]{sigplanconf}\r\n\r\n\\usepackage{url}\r\n% \\usepackage{code}\r\n\\usepackage{graphicx}\r\n\\usepackage{"
},
{
"path": "papers/haskell_symposium_2009/infrastructure.tex",
"chars": 11330,
"preview": "\\section{Profiling Infrastructure}\n\\begin{figure*}\n\\begin{center}\n\\includegraphics[scale=0.3]{eventbench.png}\n\\end{cente"
},
{
"path": "papers/haskell_symposium_2009/motivation.tex",
"chars": 8836,
"preview": "\\section{Profiling Motivation}\r\nHaskell provides a mechanism to allow the user to control the granularity of parallelism"
},
{
"path": "papers/haskell_symposium_2009/related-work.tex",
"chars": 917,
"preview": "\\section{Related Work}\r\n\r\nGranSim~\\cite{loidl} is an event-driven simulator for the parallel\r\nexecution of Glasgow Paral"
},
{
"path": "papers/haskell_symposium_2009/sigplanconf.cls",
"chars": 31815,
"preview": "%-----------------------------------------------------------------------------\n%\n% LaTeX Class/Style File\n"
},
{
"path": "papers/haskell_symposium_2009/sumEuler/Makefile",
"chars": 1106,
"preview": "GHC = /c/ghc/ghc/inplace/bin/ghc-stage2\r\nGHC_OPTS = -threaded -eventlog\r\n# HEAP = -H100M\r\nHEAP =\r\n\r\nall:\t\r\n\t$(GHC) $(GHC"
},
{
"path": "papers/haskell_symposium_2009/sumEuler/SumEuler0.hs",
"chars": 2163,
"preview": "-------------------------------------------------------------------------------\r\n-- This program runs fib and sumEuler s"
},
{
"path": "papers/haskell_symposium_2009/sumEuler/SumEuler1.hs",
"chars": 2150,
"preview": "-------------------------------------------------------------------------------\r\n-- This demonstrates that f `par` (f + "
},
{
"path": "papers/haskell_symposium_2009/sumEuler/SumEuler2.hs",
"chars": 2075,
"preview": "-------------------------------------------------------------------------------\r\n\r\nmodule Main\r\nwhere\r\nimport System.Tim"
},
{
"path": "papers/haskell_symposium_2009/sumEuler/SumEuler3.hs",
"chars": 2086,
"preview": "-------------------------------------------------------------------------------\r\n\r\nmodule Main\r\nwhere\r\nimport System.Tim"
},
{
"path": "papers/haskell_symposium_2009/threadring.tex",
"chars": 4929,
"preview": "\\subsection{Thread Ring}\n\nThe thread ring benchmark originates in the Computer Language\nBenchmarks Game\\footnote{\\url{ht"
},
{
"path": "scripts/install-on-osx.sh",
"chars": 1325,
"preview": "#!/bin/sh\n\nHC=$1\n\nset -ex\n\nCABALPKG=\"cabal-c92b4ea7ce036fae6ebf3c2965d6ecc0ef252775-20170725-123913.xz\"\nCABALCHECKSUM=\"2"
},
{
"path": "stack.osx.yaml",
"chars": 656,
"preview": "resolver: lts-16.28\npackages:\n- .\nextra-deps:\n- cairo-0.13.8.1@sha256:1938aaeb5d3504678d995774dfe870f6b66cbd43d336b692fa"
},
{
"path": "stack.yaml",
"chars": 616,
"preview": "resolver: lts-16.28\npackages:\n- .\nextra-deps:\n- cairo-0.13.8.1@sha256:1938aaeb5d3504678d995774dfe870f6b66cbd43d336b692fa"
},
{
"path": "tests/Hello.hs",
"chars": 46,
"preview": "module Main\r\nwhere\r\n\r\nmain = putStrLn \"Hello.\""
},
{
"path": "tests/Makefile",
"chars": 502,
"preview": "GHC = c:/ghc/ghc/inplace/bin/ghc-stage2\r\nGHC_OPTS = -O -threaded -eventlog\r\n\r\nall:\t\r\n\t$(GHC) $(GHC_OPTS) --make Null.hs\r"
},
{
"path": "tests/Null.hs",
"chars": 40,
"preview": "module Main\r\nwhere\r\n\r\nmain = return ()\r\n"
},
{
"path": "tests/ParFib.hs",
"chars": 1811,
"preview": "-------------------------------------------------------------------------------\r\n-- A parallel implementation of fib in "
},
{
"path": "tests/SumEulerPar1.hs",
"chars": 1906,
"preview": "-------------------------------------------------------------------------------\r\n--- $Id: SumEulerPar1.hs#1 2008/05/06 1"
},
{
"path": "threadscope.cabal",
"chars": 5234,
"preview": "Cabal-version: 1.24\nName: threadscope\nVersion: 0.2.15.0\nCategory: Developmen"
},
{
"path": "threadscope.ui",
"chars": 105043,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<interface>\n <requires lib=\"gtk+\" version=\"2.16\"/>\n <object class=\"GtkAdjustmen"
}
]
About this extraction
This page contains the full source code of the haskell/ThreadScope GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (543.7 KB), approximately 132.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.