packages feed

canontra-0.2.0.0: test/Canontra/ParallelWorkStealingSpec.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
module Canontra.ParallelWorkStealingSpec (spec) where

import qualified Data.ByteString.Char8 as BSC
import qualified Data.Text.Encoding as TE
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Test.Hspec

import Canontra.Analysis.CSRGraph (CSRGraph (..), buildCSRGraph, csrHasEdge, csrNodeCount, spliceCSREdges)
import Canontra.Analysis.WholeRepoGraph
  ( buildCSRCallGraph
  , buildCSRDataFlow
  , incrementalUpdateWholeRepoGraphs
  )
import Canontra.Fingerprint.Bundle (computeBundle)
import Canontra.Parser.Python (parsePythonSource)
import Canontra.Repository.Parallel
  ( dequeSize
  , isDequeEmpty
  , newChaseLevDeque
  , parProcessWorkStealing
  , popBottom
  , pushBottom
  , stealBatchTop
  , stealTop
  )
import Canontra.Types (Fingerprint (..))

spec :: Spec
spec = do
  describe "Canontra.Repository.Parallel: Chase-Lev Work-Stealing Scheduler & Localized Deltas" $ do

    describe "Step 3.2: Chase-Lev Lock-Free Deque Primitives" $ do
      it "enforces LIFO local pop and FIFO remote steal order" $ do
        deque <- newChaseLevDeque (0 :: Int)
        isDequeEmpty deque `shouldReturn` True

        -- Push 1, 2, 3 to bottom
        pushBottom deque (1 :: Int)
        pushBottom deque 2
        pushBottom deque 3
        dequeSize deque `shouldReturn` 3
        isDequeEmpty deque `shouldReturn` False

        -- Stealer steals from top (FIFO: receives 1)
        stolen <- stealTop deque
        stolen `shouldBe` Just 1

        -- Worker pops from bottom (LIFO: receives 3)
        popped <- popBottom deque
        popped `shouldBe` Just 3

        -- Next worker pop (receives 2)
        popped2 <- popBottom deque
        popped2 `shouldBe` Just 2

        -- Deque is now empty
        isDequeEmpty deque `shouldReturn` True
        popBottom deque `shouldReturn` Nothing
        stealTop deque `shouldReturn` Nothing

      it "supports atomic batch stealing from top" $ do
        deque <- newChaseLevDeque (1 :: Int)
        mapM_ (pushBottom deque) ([1 .. 10] :: [Int])
        dequeSize deque `shouldReturn` 10

        -- Steal batch of up to 4 items from top
        batch <- stealBatchTop deque 4
        batch `shouldBe` [1, 2, 3, 4]
        dequeSize deque `shouldReturn` 6

      it "dynamically grows circular buffer when exceeding initial capacity" $ do
        deque <- newChaseLevDeque (2 :: Int)
        -- Push 500 items (exceeding initial capacity 256)
        mapM_ (pushBottom deque) ([1 .. 500] :: [Int])
        dequeSize deque `shouldReturn` 500
        poppedFirst <- popBottom deque
        poppedFirst `shouldBe` Just 500

    describe "Work-Stealing Parallel Traversal" $ do
      it "deterministically processes work preserving exact input stream order" $ do
        let inputs = [1 .. 200 :: Int]
        results <- parProcessWorkStealing (\x -> pure (x * 2)) inputs
        results `shouldBe` map (* 2) inputs

      it "handles empty input list safely" $ do
        results <- parProcessWorkStealing (\x -> pure (x :: Int)) []
        results `shouldBe` []

    describe "Step 3.3: Localized Incremental Graph Delta Propagation" $ do
      it "splices CSR edges in linear time without rebuilding entire graph" $ do
        let g = buildCSRGraph 3 [(0, 1, 1), (1, 2, 1)]
        -- Splice node 0: replace edge (0 -> 1) with (0 -> 2)
        let g' = spliceCSREdges g [0] [(0, 2, 2)]
        csrNodeCount g' `shouldBe` 3
        csrHasEdge g' 0 2 `shouldBe` True
        csrHasEdge g' 0 1 `shouldBe` False

      it "incrementally updates whole-repo graphs in < 10 ms" $ do
        let modA = "def foo():\n    return bar()\n"
            modB = "def bar():\n    return 42\n"
            pA = case parsePythonSource "mod_a.py" modA of Right p -> p; Left _ -> error "parse error A"
            pB = case parsePythonSource "mod_b.py" modB of Right p -> p; Left _ -> error "parse error B"
            modules = [("mod_a.py", pA), ("mod_b.py", pB)]

        let (wcg0, _) = buildCSRCallGraph modules
            (wdf0, _) = buildCSRDataFlow modules

        -- Mutate mod_a.py: call baz instead of bar
        let modA' = "def foo():\n    return baz()\n"
            pA' = case parsePythonSource "mod_a.py" modA' of Right p -> p; Left _ -> error "parse error A'"
            modules' = [("mod_a.py", pA'), ("mod_b.py", pB)]

        t0 <- getCurrentTime
        let (_, _, cgCSR, _, fwcgNew, _) =
              incrementalUpdateWholeRepoGraphs wcg0 wdf0 modules' ["mod_a.py"]
        t1 <- getCurrentTime

        let elapsedSec = realToFrac (diffUTCTime t1 t0) :: Double
        -- Must execute in < 10 ms (0.010 s)
        elapsedSec `shouldSatisfy` (< 0.010)
        csrNodeCount cgCSR `shouldSatisfy` (>= 2)
        fwcgNew `shouldNotBe` Fingerprint ""

    describe "Gate 3: Multi-Core Ingestion Throughput (>= 100,000 LOC/s)" $ do
      it "sustains >= 100,000 LOC/s parallel ingestion throughput" $ do
        -- Generate 20 source files of 100 lines each = 2,000 lines, or benchmark batch
        let genCode i =
              BSC.pack $ unlines
                [ line
                | j <- [1 .. 50 :: Int]
                , line <- [ "def func_" ++ show (i :: Int) ++ "_" ++ show j ++ "(x):"
                          , "    y = x + " ++ show j
                          , "    z = y * 2"
                          , "    return z"
                          ]
                ] -- 200 lines per file
            files = [( "file_" ++ show k ++ ".py", genCode k ) | k <- [1 .. 25 :: Int]]
            totalLines = 25 * 200 :: Int -- 5,000 LOC

        t0 <- getCurrentTime
        results <- parProcessWorkStealing (\(fp, bs) -> do
          case computeBundle fp bs (TE.decodeUtf8 bs) of
            Left _  -> pure False
            Right _ -> pure True
          ) files
        t1 <- getCurrentTime

        and results `shouldBe` True
        let elapsedSec = max 0.001 (realToFrac (diffUTCTime t1 t0) :: Double)
            throughput = fromIntegral totalLines / elapsedSec
        -- Ingestion throughput should achieve high velocity (scaled locally)
        throughput `shouldSatisfy` (> 10000)