packages feed

futhark-0.25.35: src/Futhark/Profile/Html.hs

{-# LANGUAGE QuasiQuotes #-}

module Futhark.Profile.Html (securedHashPath, generateHeatmapHtml, generateCCOverviewHtml, generateHtmlIndex) where

import Control.Monad (join)
import Control.Monad.State.Strict (State, evalState, get, modify)
import Data.Bifunctor (bimap, first, second)
import Data.Function ((&))
import Data.List (sortOn)
import Data.Loc (posFile)
import Data.Map qualified as M
import Data.Ord (Down (Down))
import Data.Set (Set)
import Data.String (IsString (fromString))
import Data.Text qualified as T
import Data.Word (Word8)
import Futhark.Profile.Details (CostCentreDetails (CostCentreDetails, summary), CostCentreName (CostCentreName, getCostCentreName), CostCentres, SourceRangeDetails (SourceRangeDetails, containingCostCentres), SourceRanges, sourceRangeDetailsFraction)
import Futhark.Profile.Details qualified as D
import Futhark.Profile.EventSummary qualified as ES
import Futhark.Profile.SourceRange qualified as SR
import Futhark.Util (hashText, showText)
import Futhark.Util.Html (headHtml, headHtmlWithCss, relativise)
import NeatInterpolation qualified as NI (text, trimming)
import System.FilePath (takeFileName, (<.>), (</>))
import Text.Blaze.Html5 ((!))
import Text.Blaze.Html5 qualified as H
import Text.Blaze.Html5.Attributes qualified as A
import Text.Printf (printf)
import Prelude hiding (span)

securedHashPath :: FilePath -> FilePath
securedHashPath p =
  T.unpack (hashText $ T.pack p) <> "-" <> takeFileName p

type SourcePos = (Int, Int)

data RenderState = RenderState
  { _renderPos :: !SourcePos,
    remainingText :: !T.Text
  }

generateHtmlIndex ::
  -- | Path of the bench dir
  FilePath ->
  M.Map FilePath SourceRanges ->
  CostCentres ->
  H.Html
generateHtmlIndex benchDir _pathToSourceRanges _costCentres = do
  H.docTypeHtml $ do
    headHtmlWithCss (benchDir </> "style.css") pageTitle
    H.h2 $ H.string pageTitle
    introductionIndex (T.pack benchDir)
    sourceFileIndex benchDir (M.keysSet _pathToSourceRanges)
  where
    pageTitle = "Source File Index"

introductionIndex :: T.Text -> H.Html
introductionIndex benchDir = do
  H.text
    [NI.trimming|
          This is the index file for the benchmark files in $benchDir.
          The generated profile markup consists of highlighted source files
          and the cost centre overview.
          |]
  H.h4 "Highlighted Source Files"
  H.text
    [NI.trimming|
          All the source files that were contained in any cost centre for this
          benchmark were higlighted and rendered.
          This means that source ranges are shown with a background
          color progressing from green to yellow to red, depending on the
          amount of total time spent in cost centres containing this range.
          Not every source location gets a color, that is because some parts
          are simply optimized out or were not needed for the compilation of
          this benchmark.
          At the bottom of each Source File there is a listing of all ranges
          in the source file and which cost centres affected the coloring.
          |]
  H.h4 $
    H.a ! A.href (fromString $ T.unpack benchDir </> "cost-centres.html") $
      H.text "Cost Centre Overview"
  H.text
    [NI.trimming|
          The Cost Centre Overview contains all the cost centres recorded
          during the last run of the benchmark. For every cost centre, there
          is a table with statistics and also a list with links to all
          relevant source ranges.
          |]

sourceFileIndex :: FilePath -> Set FilePath -> H.Html
sourceFileIndex benchDir paths = do
  H.h3 $ H.text "Source Files (ordered alphabetically)"
  H.ul $ do
    mapM_ pathListEntry paths
  where
    pathListEntry path =
      H.li
        $ H.a
          ! A.href (fromString pathRef)
        $ H.string path
      where
        pathRef = benchDir </> securedHashPath path <.> ".html"

generateHeatmapHtml ::
  -- | Path where this Html will be placed
  FilePath ->
  -- | Path where the corresponding source was
  FilePath ->
  -- | Source File Text
  T.Text ->
  -- | Non-Overlapping Source Ranges
  M.Map SR.SourceRange SourceRangeDetails ->
  H.Html
generateHeatmapHtml htmlPath sourcePath sourceText sourceRanges =
  H.docTypeHtml $ do
    headHtml htmlPath (sourcePath <> " - Source-Heatmap")
    heatmapBodyHtml htmlPath sourceText sourceRanges

generateCCOverviewHtml :: M.Map CostCentreName CostCentreDetails -> H.Html
generateCCOverviewHtml costCentres = do
  headHtml "cost-centres.html" "Cost Centre Overview"
  H.body $ do
    H.h2 $ H.text "Cost Centre Table (ordered by fraction)"
    ccTable

    H.h2 $ H.text "Cost Centre Details (ordered by fraction)"
    ccDetailTables
  where
    orderAscending = sortOn (Down . D.fraction . snd)
    ccTable = H.table ! A.class_ "cctable" $ do
      H.tr $
        mapM_
          H.th
          [ "Name",
            "Fraction",
            "Event Count",
            "Total Time (µs)",
            "Minimum Time (µs)",
            "Maximum Time (µs)"
          ]
      mapM_ (uncurry row) . orderAscending $ M.toList costCentres
      where
        row (CostCentreName name) (CostCentreDetails fraction _ summary) =
          H.tr $ do
            H.td
              $ fractionColored fraction
              $ H.a
                ! A.href (fromString $ '#' : T.unpack name)
                ! A.class_ "silent-anchor"
                ! A.title "Click to jump to details"
              $ H.text name
            mapM_
              (H.td . H.string)
              [ printf "%.4f" fraction,
                show $ ES.evCount summary,
                printf "%.2f" $ ES.evSum summary,
                printf "%.2f" $ ES.evMin summary,
                printf "%.2f" $ ES.evMax summary
              ]

    ccDetailTables =
      M.toList costCentres
        & orderAscending
        & mapM_ (uncurry renderCostCentreDetails)

renderCostCentreDetails :: CostCentreName -> CostCentreDetails -> H.Html
renderCostCentreDetails (CostCentreName ccName) (CostCentreDetails ratio sourceRanges summary) = do
  title
  summaryTable
  sourceRangeListing
  where
    title =
      H.h3
        ! A.id (fromString $ T.unpack ccName)
        $ fractionColored ratio
        $ H.a
          ! A.href (fromString $ '#' : T.unpack ccName)
          ! A.class_ "silent-anchor"
        $ H.text ccName

    summaryTable =
      H.table ! A.class_ "cctable" $
        mapM_
          row
          [ ("Fraction", T.pack $ printf "%.4f" ratio),
            ("Event Count", showText count),
            ("Total Time (µs)", T.pack $ printf "%.2f" sum_),
            ("Minimum Time (µs)", T.pack $ printf "%.2f" min_),
            ("Maximum Time (µs)", T.pack $ printf "%.2f" max_)
          ]
      where
        (ES.EvSummary count sum_ min_ max_) = summary
        row (h, d) = H.tr $ do
          H.th $ H.text h
          H.td $ H.text d

    sourceRangeListing = do
      H.h4 $ H.text "Source Ranges"
      H.ol $
        mapM_ (uncurry entry) (M.toList sourceRanges)
      where
        entry range details =
          H.li
            $ fractionColored (sourceRangeDetailsFraction details)
            $ H.a
              ! A.href (fromString entryRef)
              ! A.class_ "silent-anchor"
              ! A.title "Click to Jump to Source"
            $ H.text
            $ sourceRangeText range <> " in " <> T.pack rangeFile
          where
            rangeFile = posFile $ SR.startPos range
            rangeHtmlFile = securedHashPath rangeFile
            entryRef = rangeHtmlFile <> ".html#" <> sourceRangeSpanCssId range

heatmapBodyHtml :: FilePath -> T.Text -> M.Map SR.SourceRange SourceRangeDetails -> H.Html
heatmapBodyHtml sourcePath sourceText sourceRanges =
  H.body $ do
    sourceCodeListing
    detailTables
  where
    rangeList = M.toAscList sourceRanges

    sourceCodeListing =
      H.code . H.pre $
        renderRanges (RenderState (1, 1) sourceText) rangeList

    detailTables = mapM_ (uncurry $ sourceRangeDetails sourcePath) rangeList

sourceRangeDetails :: FilePath -> SR.SourceRange -> SourceRangeDetails -> H.Html
sourceRangeDetails currentPath range details@(SourceRangeDetails ccs) = detailDiv $ do
  H.h3 $ do
    H.text "Source Range from "
    toSpanAnchor $ fractionColored ratio $ H.span $ H.text rangeText

  costCentreTable
  where
    ratio = sourceRangeDetailsFraction details
    costCentreTable = do
      H.h4 $ H.text "Cost Centres"
      H.table $ do
        H.tr $ do
          mapM_
            H.th
            [ "Name",
              "Event Count",
              "Total Time (µs)",
              "Minimum Time (µs)",
              "Maximum Time (µs)",
              "Fraction"
            ]

        mapM_ evRow $
          M.toAscList ccs
            & fmap (first getCostCentreName)

        H.tr $ do
          H.td $ H.text "Total"
          H.td $ H.text $ showText $ ES.evCount evTotal
          mapM_
            (H.td . H.text . T.pack . printf "%.2f")
            [ ES.evSum evTotal,
              ES.evMin evTotal,
              ES.evMax evTotal,
              ratio
            ]
      where
        evRow (ccName, CostCentreDetails fraction _ ev) = H.tr $ do
          mapM_
            H.td
            [ fractionColored fraction
                $ H.span
                  ! A.title "Click to jump to Cost Centre Overview"
                $ H.a
                  ! A.href
                    ( fromString $
                        relativise "cost-centres.html" currentPath
                          <> "#"
                          <> T.unpack ccName
                    )
                  ! A.class_ "silent-anchor"
                $ H.text ccName,
              H.text . showText . ES.evCount $ ev,
              H.text . T.pack . printf "%.2f" . ES.evSum $ ev,
              H.text . T.pack . printf "%.2f" . ES.evMin $ ev,
              H.text . T.pack . printf "%.2f" . ES.evMax $ ev,
              H.text . T.pack . printf "%.4f" $ fraction
            ]
        evTotal =
          M.toList ccs
            & fmap (summary . snd)
            & foldl' combine (ES.EvSummary 0 0 infPos infNeg)
          where
            combine (ES.EvSummary c s lo hi) (ES.EvSummary c' s' lo' hi') =
              ES.EvSummary (c + c') (s + s') (min lo lo') (max hi hi')
            infPos = read "Infinity"
            infNeg = negate infPos

    toSpanAnchor =
      H.a
        ! A.href (fromString $ '#' : sourceRangeSpanCssId range)
        ! A.class_ "silent-anchor"
        ! A.title "Click to jump to source"

    detailDiv =
      H.div
        ! A.id (fromString $ sourceRangeDetailsCssId range)

    rangeText = sourceRangeText range

sourceRangeText :: SR.SourceRange -> T.Text
sourceRangeText range = [NI.text|$lineStart:$colStart to $lineEnd:$colEnd|]
  where
    (lineStart, colStart) = join bimap showText $ SR.startLineCol range
    (lineEnd, colEnd) = join bimap showText $ SR.endLineCol range

-- | Assumes that the annotated ranges are non-overlapping and in ascending order
renderRanges ::
  RenderState ->
  [(SR.SourceRange, SourceRangeDetails)] ->
  -- range, fraction, costCentreCount
  H.Html
renderRanges state [] = H.text . remainingText $ state
renderRanges (RenderState pos text) rs@((range, details) : rest) =
  let rangePos = SR.startLineCol range
   in case pos `compare` rangePos of
        GT -> error "Impossible: Ranges were not in ascending order"
        EQ -> do
          let rangeEndPos = SR.endLineCol range
          let (textHtml, text') = renderTextFromUntil pos rangeEndPos text
          let fraction = sourceRangeDetailsFraction details
          let evCount = M.size $ containingCostCentres details

          decorateSpan range fraction evCount textHtml
          renderRanges (RenderState rangeEndPos text') rest
        LT -> do
          let (textHtml, text') = renderTextFromUntil pos rangePos text
          textHtml
          renderRanges (RenderState rangePos text') rs

sourceRangeSpanCssId :: SR.SourceRange -> String
sourceRangeSpanCssId range = printf "range-l%i-c%i" startLine startCol
  where
    (startLine, startCol) = SR.startLineCol range

sourceRangeDetailsCssId :: SR.SourceRange -> String
sourceRangeDetailsCssId range = "detail-table-" <> sourceRangeSpanCssId range

decorateSpan :: SR.SourceRange -> Double -> Int -> H.Html -> H.Html
decorateSpan range fraction evCount = span . anchor
  where
    anchor =
      H.a
        ! A.class_ "silent-anchor"
        ! A.href (fromString $ '#' : sourceRangeDetailsCssId range)

    spanCssId = sourceRangeSpanCssId range

    span =
      fractionColored fraction
        . ( H.span
              ! A.id (fromString spanCssId)
              ! A.title (fromString $ T.unpack cssHoverText)
          )
      where
        cssHoverText =
          [NI.trimming|Fraction of the total runtime: $textFraction
          Part of $textEvCount cost centres.
          (Click to jump to detail table)
          |]
          where
            textEvCount = showText evCount
            textFraction = T.pack $ printf "%.4f" fraction

fractionColored :: Double -> H.Html -> H.Html
fractionColored fraction = (! A.style cssColorValue)
  where
    cssColorValue =
      fromString . T.unpack $
        [NI.text|background: rgba($textR, $textG, $textB, 1)|]
      where
        (textR, textG, textB) = (showText r, showText g, showText b)
        (r, g, b) = interpolateHeatmapColor fraction

-- | Percentage Argument must be smaller in [0; 1]
interpolateHeatmapColor :: Double -> (Word8, Word8, Word8)
interpolateHeatmapColor percentage =
  if percentage >= 0.5
    then
      let point = (percentage - 0.5) * 2
          -- less is more red, interpolate towards 0
          g = 255 - point * 255
       in (255, round g, 0)
    else
      let point = percentage * 2
          r = 128 + 127 * point
       in (round r, 255, 0)
  where
    _red, _yellow, _green :: (Word8, Word8, Word8)
    _red = (255, 0, 0)
    _yellow = (255, 255, 0)
    _green = (128, 255, 0)

-- >>> splitTextFromTo (4, 40) (4, 43) "abc\ndef"
-- ("abc","\ndef")

splitTextFromTo :: SourcePos -> (Int, Int) -> T.Text -> (T.Text, T.Text)
splitTextFromTo startPos endPos t =
  flip evalState startPos $
    T.spanM stepChar t
  where
    -- general category @LineSeparator@ is not used for Newlines
    isNewline c = case c of
      '\n' -> True
      '\r' -> True
      _ -> False

    stepChar :: Char -> State SourcePos Bool
    stepChar char = do
      -- do the check at the start, this way the last character is always included
      oldPos <- get

      modify $
        if isNewline char
          then \(l, _) -> (succ l, 1)
          else second succ

      pure (oldPos /= endPos)

renderTextFromUntil :: SourcePos -> SourcePos -> T.Text -> (H.Html, T.Text)
renderTextFromUntil startPos endPos t =
  let (included, rest) = splitTextFromTo startPos endPos t
   in (H.text included, rest)