packages feed

moonlight-triangulation-1.4.0.4: bench/power/Moonlight/Triangulation/PowerBench.hs

-- | Tasty-bench lanes for the exact bounded power-diagram owner.  The timed
-- lanes force the public result, while the one-shot report keeps the
-- structural receipt and a shared incremental allocation observation outside
-- the timing sample.  One construction is revealed, its diagram is forced,
-- and only then is the additional receipt-reachable section measured.
module Moonlight.Triangulation.PowerBench
  ( PowerBenchCase
  , prepareCases
  , reportCases
  , runBenchmarks
  ) where

import BenchSupport (randomPoints, requireRight)
import Control.DeepSeq (force)
import Control.Exception (evaluate)
import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import GHC.Conc.Sync (getAllocationCounter, setAllocationCounter)
import Moonlight.Triangulation
  ( BoundedPowerDiagram
  , ConvexPolygon
  , Point (Point)
  , PowerDiagramError
  , PowerDiagramReceipt
  , PowerSite
  , RegularTriangulation
  , RegularTriangulationReceipt
  , boundedPowerDiagram
  , convexPolygon
  , exactPoint
  , powerDiagramActiveBoundaries
  , powerDiagramCoincidentDominatedCells
  , powerDiagramCoincidentEquivalentCells
  , powerDiagramDomainVertices
  , powerDiagramBoundaryCompatibilityChecks
  , powerDiagramEmptyCells
  , powerDiagramExactIntersections
  , powerDiagramFinalCoordinateBitGrowth
  , powerDiagramFinalCoordinateBits
  , powerDiagramFinalDenominatorBits
  , powerDiagramInputSites
  , powerDiagramLowerDimensionalCells
  , powerDiagramMaximumAffineCoefficientBits
  , powerDiagramMaximumInputBits
  , powerDiagramPeakIntermediateBitGrowth
  , powerDiagramPeakIntermediateCoordinateBits
  , powerDiagramMaximumCellConstraints
  , powerDiagramPublishedCells
  , powerDiagramOracleCells
  , powerDiagramRegularEdges
  , powerDiagramRegularFaces
  , powerDiagramSubmittedSiteConstraints
  , powerSite
  , powerWeight
  , regularTriangulation
  )
import System.Mem (performGC)
import Test.Tasty.Bench (Benchmark, bench, bgroup, defaultMain, nf)

data PowerBenchFamily
  = WellConditioned
  | NearParallel
  | CollinearSlopes
  deriving stock (Eq, Ord, Show)

data PowerBenchCase = PowerBenchCase
  { powerBenchLabel :: !String
  , powerBenchDomain :: !ConvexPolygon
  , powerBenchSites :: !(NonEmpty (PowerSite Int))
  }

type PowerResult =
  Either (PowerDiagramError Int) (BoundedPowerDiagram Int, PowerDiagramReceipt)

type DiagramResult =
  Either (PowerDiagramError Int) (BoundedPowerDiagram Int)

type RegularResult =
  Either
    (PowerDiagramError Int)
    (RegularTriangulation Int, RegularTriangulationReceipt)

siteCounts :: [Int]
siteCounts = [16, 64, 128, 169, 256, 512]

families :: [PowerBenchFamily]
families = [WellConditioned, NearParallel, CollinearSlopes]

-- | Deterministic, fixed-domain fixture bands include the exhibit scale and a
-- larger scaling point.  Ordinary and nearly parallel families expose exact
-- clipping cost; the collinear family exposes upper-chain classification.
prepareCases :: IO [PowerBenchCase]
prepareCases =
  traverse prepareCase [(family, count) | family <- families, count <- siteCounts]

prepareCase :: (PowerBenchFamily, Int) -> IO PowerBenchCase
prepareCase (family, siteCount) = do
  domain <-
    requireRight
      ( convexPolygon
          ( exactPoint (-1) (-1)
              :| [exactPoint 1 (-1), exactPoint 1 1, exactPoint (-1) 1]
          )
      )
  sites <-
    traverse (prepareSite family) (zip [0 ..] (fixturePoints family siteCount))
  case NonEmpty.nonEmpty sites of
    Nothing -> fail "power benchmark fixture requires at least one site"
    Just nonEmptySites ->
      pure
        PowerBenchCase
          { powerBenchLabel = familyLabel family <> "/n=" <> show siteCount
          , powerBenchDomain = domain
          , powerBenchSites = nonEmptySites
          }

fixturePoints :: PowerBenchFamily -> Int -> [Point]
fixturePoints family siteCount =
  case family of
    WellConditioned -> randomPoints 0x9e3779b97f4a7c15 siteCount
    NearParallel -> nearParallelPoints siteCount
    CollinearSlopes -> collinearSlopePoints siteCount

nearParallelPoints :: Int -> [Point]
nearParallelPoints siteCount =
  [ Point
      (fromIntegral index / fromIntegral (siteCount + 1) * 1.8 - 0.9)
      (fromIntegral index / fromIntegral (siteCount + 1) * 1.8e-6 - 0.9e-6)
  | index <- [1 .. siteCount]
  ]

collinearSlopePoints :: Int -> [Point]
collinearSlopePoints siteCount =
  [ Point (fromIntegral index / 1024 - 0.5) 0
  | index <- [0 .. siteCount - 1]
  ]

familyLabel :: PowerBenchFamily -> String
familyLabel WellConditioned = "power-well-conditioned"
familyLabel NearParallel = "power-near-parallel"
familyLabel CollinearSlopes = "power-collinear-slopes"

prepareSite :: PowerBenchFamily -> (Int, Point) -> IO (PowerSite Int)
prepareSite family (label, point) = do
  let weightValue =
        case family of
          CollinearSlopes
            | odd label -> -(2 / 1048576)
            | otherwise -> 0
          WellConditioned -> fromIntegral (label `mod` 9 - 4) / 256
          NearParallel -> fromIntegral (label `mod` 9 - 4) / 256
  weight <- requireRight (powerWeight weightValue)
  requireRight (powerSite label point weight)

-- | Print one receipt and one allocation probe per fixture.  Tasty-bench
-- remains the authority for timing; this report is the derived structural
-- view that timing samples cannot carry.
reportCases :: [PowerBenchCase] -> IO ()
reportCases cases = do
  putStrLn "moonlight-triangulation exact bounded power-diagram benchmark"
  putStrLn
    "receipt-overhead-method: reveal one shared construction, force its diagram, then measure only the additional receipt-reachable allocation; strict work incurred while revealing the pair is not attributed"
  traverse_ reportCase cases

reportCase :: PowerBenchCase -> IO ()
reportCase benchmarkCase = do
  (sharedResult, allocation) <- measureSharedAllocation benchmarkCase
  case sharedResult of
    Left failure -> fail (powerBenchLabel benchmarkCase <> ": " <> show failure)
    Right (_, receipt) -> do
      putStrLn (renderReceipt (powerBenchLabel benchmarkCase) receipt)
      putStrLn
        ( powerBenchLabel benchmarkCase
            <> "-allocated-bytes/shared-full: "
            <> show (sharedFullAllocation allocation)
        )
      putStrLn
        ( powerBenchLabel benchmarkCase
            <> "-allocated-bytes/shared-through-diagram: "
            <> show (sharedDiagramAllocation allocation)
        )
      putStrLn
        ( powerBenchLabel benchmarkCase
            <> "-allocated-bytes/incremental-receipt-reachable-overhead: "
            <> show (sharedReceiptReachableAllocation allocation)
        )

runBenchmarks :: [PowerBenchCase] -> IO ()
runBenchmarks cases = defaultMain [bgroup "exact bounded power diagram" (concatMap caseBenchmarks cases)]

caseBenchmarks :: PowerBenchCase -> [Benchmark]
caseBenchmarks benchmarkCase =
  [ bench
      (powerBenchLabel benchmarkCase <> "/regular-topology")
      (nf constructRegularTopology benchmarkCase)
  , bench
      (powerBenchLabel benchmarkCase <> "/construct-with-receipt")
      (nf construct benchmarkCase)
  , bench
      (powerBenchLabel benchmarkCase <> "/construct-diagram-only")
      (nf constructDiagramOnly benchmarkCase)
  ]

construct :: PowerBenchCase -> PowerResult
construct benchmarkCase =
  boundedPowerDiagram
    (powerBenchDomain benchmarkCase)
    (powerBenchSites benchmarkCase)

constructDiagramOnly :: PowerBenchCase -> DiagramResult
constructDiagramOnly = fmap fst . construct

constructRegularTopology :: PowerBenchCase -> RegularResult
constructRegularTopology = regularTriangulation . powerBenchSites

data SharedPowerAllocation = SharedPowerAllocation
  { sharedFullAllocation :: !Integer
  , sharedDiagramAllocation :: !Integer
  , sharedReceiptReachableAllocation :: !Integer
  }

measureSharedAllocation
  :: PowerBenchCase
  -> IO (PowerResult, SharedPowerAllocation)
measureSharedAllocation benchmarkCase = do
  performGC
  setAllocationCounter maxBound
  allocationStart <- getAllocationCounter
  sharedResult <- evaluate (construct benchmarkCase)
  case sharedResult of
    Left failure -> do
      forcedFailure <- evaluate (force failure)
      allocationEnd <- getAllocationCounter
      let total = allocationDifference allocationStart allocationEnd
      pure
        ( Left forcedFailure
        , SharedPowerAllocation total total 0
        )
    Right (diagram, receipt) -> do
      forcedDiagram <- evaluate (force diagram)
      allocationAfterDiagram <- getAllocationCounter
      forcedReceipt <- evaluate (force receipt)
      allocationAfterReceipt <- getAllocationCounter
      pure
        ( Right (forcedDiagram, forcedReceipt)
        , SharedPowerAllocation
            { sharedFullAllocation =
                allocationDifference allocationStart allocationAfterReceipt
            , sharedDiagramAllocation =
                allocationDifference allocationStart allocationAfterDiagram
            , sharedReceiptReachableAllocation =
                allocationDifference allocationAfterDiagram allocationAfterReceipt
            }
        )

allocationDifference :: Integral amount => amount -> amount -> Integer
allocationDifference before after = toInteger (max 0 (before - after))

renderReceipt :: String -> PowerDiagramReceipt -> String
renderReceipt label receipt =
  unwords
    [ label <> "-receipt:"
    , field "input-sites" powerDiagramInputSites
    , field "domain-vertices" powerDiagramDomainVertices
    , field "submitted-site-constraints" powerDiagramSubmittedSiteConstraints
    , field "active-boundaries" powerDiagramActiveBoundaries
    , field "boundary-compatibility-checks" powerDiagramBoundaryCompatibilityChecks
    , field "exact-intersections" powerDiagramExactIntersections
    , field "published" powerDiagramPublishedCells
    , field "lower-dimensional" powerDiagramLowerDimensionalCells
    , field "empty" powerDiagramEmptyCells
    , field "coincident-equivalent" powerDiagramCoincidentEquivalentCells
    , field "coincident-dominated" powerDiagramCoincidentDominatedCells
    , field "regular-faces" powerDiagramRegularFaces
    , field "regular-edges" powerDiagramRegularEdges
    , field "oracle-cells" powerDiagramOracleCells
    , field "maximum-cell-constraints" powerDiagramMaximumCellConstraints
    , field "input-bits" powerDiagramMaximumInputBits
    , field "affine-bits" powerDiagramMaximumAffineCoefficientBits
    , field "peak-coordinate-bits" powerDiagramPeakIntermediateCoordinateBits
    , field "final-coordinate-bits" powerDiagramFinalCoordinateBits
    , field "final-denominator-bits" powerDiagramFinalDenominatorBits
    , field "peak-growth-bits" powerDiagramPeakIntermediateBitGrowth
    , field "final-growth-bits" powerDiagramFinalCoordinateBitGrowth
    ]
 where
  field :: Show value => String -> (PowerDiagramReceipt -> value) -> String
  field name project = name <> "=" <> show (project receipt)