diff --git a/flake.lock b/flake.lock index 0a69d49..4429546 100644 --- a/flake.lock +++ b/flake.lock @@ -18,6 +18,39 @@ "type": "github" } }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-root": { + "locked": { + "lastModified": 1723604017, + "narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=", + "owner": "srid", + "repo": "flake-root", + "rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "flake-root", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -89,6 +122,27 @@ "type": "github" } }, + "nixd": { + "inputs": { + "flake-parts": "flake-parts_2", + "flake-root": "flake-root", + "nixpkgs": "nixpkgs_3", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1765393498, + "narHash": "sha256-OapXNkISC2JXOFs7c6V7puiYg4+XSJxbGNce3GNVMWo=", + "owner": "nix-community", + "repo": "nixd", + "rev": "72f4dd1fc57114dd0b455d07605de899d6961f29", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixd", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1702539185, @@ -123,6 +177,18 @@ "type": "github" } }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1733096140, + "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + } + }, "nixpkgs_2": { "locked": { "lastModified": 1748984911, @@ -139,6 +205,22 @@ } }, "nixpkgs_3": { + "locked": { + "lastModified": 1754800730, + "narHash": "sha256-HfVZCXic9XLBgybP0318ym3cDnGwBs/+H5MgxFVYF4I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "641d909c4a7538f1539da9240dedb1755c907e40", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { "locked": { "lastModified": 1684212024, "narHash": "sha256-/3ZvkPuIXdyZqPR53qC7aaV5wiwMOY+ddbESOykZ9Vo=", @@ -160,7 +242,8 @@ "haskell-flake": "haskell-flake", "hell": "hell", "nix2container": "nix2container", - "nixpkgs": "nixpkgs_3" + "nixd": "nixd", + "nixpkgs": "nixpkgs_4" } }, "systems": { @@ -177,6 +260,27 @@ "repo": "default", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixd", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1734704479, + "narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index b37fdbc..87f9609 100644 --- a/flake.nix +++ b/flake.nix @@ -5,9 +5,10 @@ haskell-flake.url = "github:srid/haskell-flake"; hell.url = "github:chrisdone/hell?ref=551133cecdafed1d6d3f4da7d8a466df2eed8af5"; nix2container.url = "github:nlewo/nix2container"; + nixd.url = "github:nix-community/nixd"; }; - outputs = inputs@{ self, nixpkgs, flake-parts, hell, nix2container, ... }: + outputs = inputs@{ self, nixpkgs, flake-parts, hell, nix2container, nixd, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = nixpkgs.lib.systems.flakeExposed; imports = [ inputs.haskell-flake.flakeModule ]; @@ -42,7 +43,7 @@ hpack = hp.hpack; webook = pkgs.webhook; hell = hell.packages.${system}.default; - + nixd = nixd.packages.${system}.default; }; }; diff --git a/src/Perf/Web/Chart.hs b/src/Perf/Web/Chart.hs index ca81c85..9187562 100644 --- a/src/Perf/Web/Chart.hs +++ b/src/Perf/Web/Chart.hs @@ -1,25 +1,31 @@ module Perf.Web.Chart where import Data.Aeson -import Data.Text (Text) import Data.ByteString.Lazy as L -import qualified Data.Text as T -import qualified Data.Text.Encoding as T +import Data.Text (Text) +import Data.Text qualified as T +import Data.Text.Encoding qualified as T import Lucid -chart_ :: Text -> Value -> Html () -chart_ idx config = do - div_ [class_ "width: 400px; height: 400px; margin: 0 auto; padding: 20px;"] do - canvas_ [id_ idx, style_ "height: 250px; max-width: 400px;"] do - pure () +chart_ :: Text -> Value -> Value -> Html () +chart_ idx plotData layout = do + div_ [id_ idx, style_ "height: 300px; max-width: 400px;"] do + pure () script_ $ - T.concat [ - "(() => {", - "const config = ", encode' config, ";", - "const ctx = document.getElementById(", encode' idx, ").getContext('2d');", - "new Chart(ctx, config);" - , "})();" - ] + T.concat + [ "(() => {", + "const data = ", + encode' plotData, + ";", + "const layout = ", + encode' layout, + ";", + "const config = {responsive: true, modeBarButtonsToRemove: ['select2d', 'lasso2d']};", + "Plotly.newPlot(", + encode' idx, + ", data, layout, config);", + "})();" + ] encode' :: ToJSON a => a -> Text encode' = T.decodeUtf8 . L.toStrict . encode diff --git a/src/Perf/Web/Layout.hs b/src/Perf/Web/Layout.hs index 5e677c8..0908496 100644 --- a/src/Perf/Web/Layout.hs +++ b/src/Perf/Web/Layout.hs @@ -19,21 +19,11 @@ defaultLayout_ title body = do "table.metrics td, table.metrics th {border: 1px solid black; padding: 2px;}" ] script_ - [ src_ "https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js", - integrity_ "sha256-SERKgtTty1vsDxll+qzd4Y2cF9swY9BCq62i9wXJ9Uo=", + [ src_ "https://cdn.jsdelivr.net/npm/plotly.js-dist-min@2.35.2/plotly.min.js", crossorigin_ "anonymous", makeAttributes "referrerpolicy" "no-referrer" ] (mempty :: Text) - script_ - [ src_ "https://cdn.jsdelivr.net/npm/chartjs-plugin-crosshair@2.0.0/dist/chartjs-plugin-crosshair.min.js", - integrity_ "sha256-5bTtdEYtbjO36pQbMCXOsoYW5u5jfYfyI41LelMTTbQ=", - crossorigin_ "anonymous", - makeAttributes "referrerpolicy" "no-referrer" - ] - (mempty :: Text) - script_ [type_ "text/javascript"] $ do - toHtmlRaw ("Chart.defaults.font.family = 'monospace';" :: Text) body_ do h1_ $ toHtml title crumbs <- asks (.crumbs) diff --git a/src/Perf/Web/Routes.hs b/src/Perf/Web/Routes.hs index 45336e9..75f281e 100644 --- a/src/Perf/Web/Routes.hs +++ b/src/Perf/Web/Routes.hs @@ -131,88 +131,52 @@ generatePlots benchmarks = do let dataSets = map (second (maybe [] Map.elems . Map.lookup metricLabel)) $ Map.toList tests - chart_ (T.pack (show b_i) <> "-" <> T.pack (show m_i)) $ - makeChartConfig metricLabel labels dataSets + let (plotData, layout) = makePlotlyConfig metricLabel labels dataSets + chart_ (T.pack (show b_i) <> "-" <> T.pack (show m_i)) plotData layout -makeChartConfig :: +makePlotlyConfig :: Prim.MetricLabel -> Set DB.Commit -> [(Set Prim.GeneralFactor, [DB.Metric])] -> - Value -makeChartConfig metricName commits dataSets = - object - [ "type" .= ("line" :: Text), - "data" .= chartData, - "options" - .= object - [ "responsive" .= True, - "maintainAspectRatio" .= True, - "animations" .= False, - "plugins" - .= object - [ "title" - .= object - [ "display" .= True, - "text" .= coerce @_ @Text metricName, - "font" - .= object - [ "size" .= (16 :: Int) - ] - ], - "tooltip" - .= object - [ "mode" .= ("point" :: Text), - "intersect" .= False - ], - "crosshair" - .= object - [ "sync" - .= object - [ "enabled" .= True, - "group" .= (1 :: Int) - ], - "zoom" - .= object - [ "enabled" .= True - ] - ] - ], - "scales" - .= object - [ "x" - .= object - [ "title" - .= object - [ "display" .= False, - "text" .= ("Commits" :: Text) - ] - ], - "y" - .= object - [ "title" - .= object - [ "display" .= True, - "text" .= coerce @_ @Text metricName - ], - "beginAtZero" .= True - ] - ] - ] - ] + (Value, Value) +makePlotlyConfig metricName commits dataSets = + (toJSON traces, layout) where - chartData = + commitLabels = List.map (T.take 8 . (coerce :: Prim.Hash -> Text) . (.commitHash)) (Set.toList commits) + traces = + [ object + [ "x" .= commitLabels, + "y" .= fillLeft (Set.size commits) Null (map (toJSON . (.metricMean)) metrics :: [Value]), + "type" .= ("scatter" :: Text), + "mode" .= ("lines+markers" :: Text), + "name" .= factorsSmall factors, + "line" .= object ["color" .= color] + ] + | ((factors, metrics), color) <- zip dataSets $ cycle colors + ] + layout = object - [ "labels" .= List.map (T.take 8 . (coerce :: Prim.Hash -> Text) . (.commitHash)) (Set.toList commits), - "datasets" - .= [ object - [ "label" .= factorsSmall factors, - "data" .= fillLeft (Set.size commits) Null (map (toJSON . (.metricMean)) metrics :: [Value]), - "borderColor" .= color, - "tension" .= (0.1 :: Double), - "fill" .= False - ] - | ((factors, metrics), color) <- zip dataSets $ cycle colors - ] + [ "title" + .= object + [ "text" .= coerce @_ @Text metricName, + "font" .= object ["family" .= ("monospace" :: Text), "size" .= (16 :: Int)] + ], + "xaxis" + .= object + [ "title" .= ("" :: Text), + "tickfont" .= object ["family" .= ("monospace" :: Text)] + ], + "yaxis" + .= object + [ "title" .= coerce @_ @Text metricName, + "rangemode" .= ("tozero" :: Text), + "tickfont" .= object ["family" .= ("monospace" :: Text)] + ], + "font" .= object ["family" .= ("monospace" :: Text)], + "hovermode" .= ("x unified" :: Text), + "showlegend" .= True, + "legend" .= object ["x" .= (1 :: Int), "y" .= (0 :: Int), "xanchor" .= ("right" :: Text), "bgcolor" .= ("rgba(0,0,0,0)" :: Text), "font" .= object ["color" .= ("rgba(0,0,0,0.4)" :: Text)]], + "margin" .= object ["t" .= (40 :: Int), "b" .= (40 :: Int), "l" .= (60 :: Int), "r" .= (20 :: Int)] ] colors :: [Text] = T.words