summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTom Smeding <tom@tomsmeding.com>2026-06-28 12:47:28 +0200
committerTom Smeding <tom@tomsmeding.com>2026-06-28 12:50:26 +0200
commit352f64c7171cf62f2e1a7578fb8e786dead90d9f (patch)
tree2fcb0663f1509b2fe5d1f2533f1e8859ddac36ad /src
parent08e042b949ca358a86c256d137379e76f3881bfc (diff)
Prototype compressed event listing
Diffstat (limited to 'src')
-rw-r--r--src/ImmutGrowVector.hs44
-rw-r--r--src/Index.hs291
-rw-r--r--src/Main.hs30
-rw-r--r--src/Pages.hs2
-rw-r--r--src/ZNC.hs1
5 files changed, 272 insertions, 96 deletions
diff --git a/src/ImmutGrowVector.hs b/src/ImmutGrowVector.hs
index d36209d..ee38bc4 100644
--- a/src/ImmutGrowVector.hs
+++ b/src/ImmutGrowVector.hs
@@ -1,43 +1,47 @@
module ImmutGrowVector where
-import Foreign.Storable
-import Data.Vector.Storable qualified as VS
+import Data.Vector.Unboxed (Unbox)
+import Data.Vector.Unboxed qualified as VU
-- | Acts like an immutable storable vector, except that it's split in a long
-- prefix and a short suffix so that modifications at the end are cheap. If the
-- suffix gets longer (by appending elements), the suffix elements are promoted
-- to prefix elements once in a while, resulting in a big copy at those times.
-data ImmutGrowVector a = ImmutGrowVector (VS.Vector a) (VS.Vector a)
+data ImmutGrowVector a = ImmutGrowVector (VU.Vector a) (VU.Vector a)
deriving (Show)
-empty :: Storable a => ImmutGrowVector a
-empty = ImmutGrowVector VS.empty VS.empty
+empty :: Unbox a => ImmutGrowVector a
+empty = ImmutGrowVector VU.empty VU.empty
-fromListN :: Storable a => Int -> [a] -> ImmutGrowVector a
+fromListN :: Unbox a => Int -> [a] -> ImmutGrowVector a
fromListN n l
| n > 2 =
let (l1, l2) = splitAt (n - 2) l
- in ImmutGrowVector (VS.fromListN (n - 2) l1) (VS.fromListN 2 l2)
+ in ImmutGrowVector (VU.fromListN (n - 2) l1) (VU.fromListN 2 l2)
| otherwise =
- ImmutGrowVector VS.empty (VS.fromListN n l)
+ ImmutGrowVector VU.empty (VU.fromListN n l)
-(!) :: Storable a => ImmutGrowVector a -> Int -> a
+(!) :: Unbox a => ImmutGrowVector a -> Int -> a
ImmutGrowVector prefix suffix ! i
- | i < VS.length prefix = prefix VS.! i
- | otherwise = suffix VS.! (i - VS.length prefix)
+ | i < VU.length prefix = prefix VU.! i
+ | otherwise = suffix VU.! (i - VU.length prefix)
-length :: Storable a => ImmutGrowVector a -> Int
-length (ImmutGrowVector prefix suffix) = VS.length prefix + VS.length suffix
+length :: Unbox a => ImmutGrowVector a -> Int
+length (ImmutGrowVector prefix suffix) = VU.length prefix + VU.length suffix
-set :: Storable a => ImmutGrowVector a -> Int -> a -> ImmutGrowVector a
+set :: Unbox a => ImmutGrowVector a -> Int -> a -> ImmutGrowVector a
set (ImmutGrowVector prefix suffix) idx value
- | idx < VS.length prefix = error "ImmutGrowVector: mutation in slow part"
- | otherwise = ImmutGrowVector prefix (suffix VS.// [(idx - VS.length prefix, value)])
+ | idx < VU.length prefix = error "ImmutGrowVector: mutation in slow part"
+ | otherwise = ImmutGrowVector prefix (suffix VU.// [(idx - VU.length prefix, value)])
-append :: Storable a => ImmutGrowVector a -> a -> ImmutGrowVector a
+append :: Unbox a => ImmutGrowVector a -> a -> ImmutGrowVector a
append (ImmutGrowVector prefix suffix) value
- | VS.length suffix < 8 = ImmutGrowVector prefix (suffix `VS.snoc` value)
+ | VU.length suffix < 8 = ImmutGrowVector prefix (suffix `VU.snoc` value)
| otherwise =
- let n = VS.length suffix
- in ImmutGrowVector (prefix <> VS.take (n - 1) suffix) (VS.drop (n - 1) suffix `VS.snoc` value)
+ let n = VU.length suffix
+ in ImmutGrowVector (prefix <> VU.take (n - 1) suffix) (VU.drop (n - 1) suffix `VU.snoc` value)
+
+-- | Transform the vector by mapping the underlying unboxed vectors
+mapUVector :: (VU.Vector a -> VU.Vector b) -> ImmutGrowVector a -> ImmutGrowVector b
+mapUVector f (ImmutGrowVector prefix suffix) = ImmutGrowVector (f prefix) (f suffix)
diff --git a/src/Index.hs b/src/Index.hs
index 94c3f9e..3703f65 100644
--- a/src/Index.hs
+++ b/src/Index.hs
@@ -1,10 +1,17 @@
{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
module Index (
Index,
EventID,
+ CountKind(..),
initIndex,
indexGetEventsLinear,
findEventIDLinear,
@@ -25,14 +32,19 @@ import Data.ByteString (ByteString)
import Data.Char (isDigit, chr, ord)
import Data.Functor ((<&>))
import Data.IORef
-import Data.List (sort, scanl', foldl', minimumBy)
+import Data.List (sort, scanl', foldl', minimumBy, intercalate)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (catMaybes)
+import Data.Monoid (Endo(..))
import Data.Ord (comparing)
import Data.Text qualified as T
import Data.Text (Text)
import Data.Time
+import Data.Vector.Generic qualified as VG
+import Data.Vector.Generic.Mutable qualified as VGM
+import Data.Vector.Unboxed qualified as VU
+import Data.Vector.Unboxed.Base qualified as VU (Vector(V_2))
import Data.Vector.Storable qualified as VS
import Data.Word
import System.Clock qualified as Clock
@@ -52,10 +64,62 @@ import Util
import ZNC
+-- This module keeps an index both for the full list of events, as well as a
+-- /compressed/ list of events. Compression here means that sequences of events
+-- with type not in a list of "important" events (see 'isImportant') are
+-- compressed into a single pseudo-event, recording the existence of
+-- unimportant events but not their details. This compression happens within a
+-- single UTC day, so if there is a sequence of unimportant events crossing a
+-- UTC day boundary, the compressed stream will have two compressed events.
+-- Ideally these would also be compressed but that would complicate the code.
+--
+-- The 'Counts' data type represents the number of events in a particular time
+-- period, as counted in each of the tracked ways (two for now).
+
+
+data CountKind = CKAll | CKCompressed
+ deriving (Show, Eq)
+
+data Counts = Counts { coAll :: {-# UNPACK #-} !Int
+ , coCmpr :: {-# UNPACK #-} !Int }
+ deriving (Show, Eq)
+
+instance Num Counts where
+ Counts a i + Counts a' i' = Counts (a + a') (i + i')
+ Counts a i - Counts a' i' = Counts (a - a') (i - i')
+ (*) = error "(*) on Counts"
+ abs = error "abs on Counts"
+ signum = error "signum on Counts"
+ fromInteger 0 = Counts 0 0
+ fromInteger _ = error "non-zero fromInteger on Counts"
+
+instance VU.IsoUnbox Counts (Int, Int) where
+ toURepr (Counts a i) = (a, i) ; {-# INLINE toURepr #-}
+ fromURepr (a, i) = Counts a i ; {-# INLINE fromURepr #-}
+newtype instance VU.MVector s Counts = MV_Counts (VU.MVector s (Int, Int))
+newtype instance VU.Vector Counts = V_Counts (VU.Vector (Int, Int))
+deriving via (Counts `VU.As` (Int, Int)) instance VGM.MVector VU.MVector Counts
+deriving via (Counts `VU.As` (Int, Int)) instance VG.Vector VU.Vector Counts
+instance VU.Unbox Counts
+
+getCount :: CountKind -> Counts -> Int
+getCount CKAll = coAll
+getCount CKCompressed = coCmpr
+
+getCountVector :: CountKind -> VU.Vector Counts -> VU.Vector Int
+getCountVector kind (V_Counts (VU.V_2 _ vAll vCmpr)) =
+ case kind of
+ CKAll -> vAll
+ CKCompressed -> vCmpr
+
+
+-- | The index for a single channel.
data ChanIndex = ChanIndex
{ ciStartDay :: !Day
- , ciCountUntil :: !(IGV.ImmutGrowVector Int) -- ^ number of events up to and /including/ this day
- , ciTotal :: !Int }
+ , ciCountUntil :: !(IGV.ImmutGrowVector Counts)
+ -- ^ Number of events up to and /including/ this day. Values:
+ -- (all events, compressed events).
+ , ciTotal :: !Counts }
deriving (Show)
-- | Inclusive.
@@ -87,6 +151,7 @@ initIndex basedir toimport = do
c_start <- Clock.getTime Clock.Realtime
items <-
forM toimport $ \(Channel nwT chT) -> do
+ atomicPrint $ "Indexing " <> nwT <> "/" <> chT
let nw = T.unpack nwT
ch = T.unpack chT
files <- listDirectory (basedir </> nw </> ch)
@@ -98,13 +163,15 @@ initIndex basedir toimport = do
-- atomicPrintS $ "Parsing " ++ path ++ " (" ++ show date ++ " -> " ++ show (dateToDay date) ++ ")"
events <- parseLog <$> BS.readFile path
let !nevents = length events
- return (uncurry3 fromGregorian date, nevents)
- let minday = minimum (map fst days)
- maxday = maximum (map fst days)
+ !ccpr = countCompressed (map snd events)
+ return (uncurry3 fromGregorian date, nevents, ccpr)
+ let minday = minimum [day | (day, _, _) <- days]
+ maxday = maximum [day | (day, _, _) <- days]
ndays = fromIntegral @Integer @Int (diffDays maxday minday + 1)
-- traceM $ nw ++ "/" ++ ch ++ ": days = " ++ show [(toFileName (dayToYMD d), i) | (d, i) <- days]
- let countScan = IGV.fromListN ndays (drop 1 $ scanl' (+) 0 (makeCounts [minday..maxday] days))
- ntotal = sum (map snd days)
+ let daysEvents = [(day, Counts nev ncompr) | (day, nev, ncompr) <- days]
+ countScan = IGV.fromListN ndays (drop 1 $ scanl' (+) 0 (makeCounts [minday..maxday] daysEvents))
+ ntotal = sum [Counts nev ncompr | (_, nev, ncompr) <- days]
chanindex <- newIORef $!
ChanIndex { ciStartDay = minday
, ciCountUntil = countScan
@@ -150,7 +217,7 @@ initIndex basedir toimport = do
return index
-makeCounts :: [Day] -> [(Day, Int)] -> [Int]
+makeCounts :: Num a => [Day] -> [(Day, a)] -> [a]
makeCounts [] [] = []
makeCounts [] _ = error "makeCounts: more entries than days in range"
makeCounts (d:ds) ents@((d',n):ents')
@@ -159,6 +226,56 @@ makeCounts (d:ds) ents@((d',n):ents')
| otherwise = error $ "makeCounts: duplicate entry? " ++ show (d, toGregorian d, d', toGregorian d')
makeCounts (_:ds) [] = 0 : makeCounts ds []
+countCompressed :: [Event] -> Int
+countCompressed = goI 0
+ where
+ goI !n [] = n
+ goI n (e:es)
+ | isImportant e = goI (n+1) es
+ | otherwise = goU n es
+
+ goU !n [] = n + 1 -- 1 for the unimportant sequence
+ goU n (e:es)
+ | isImportant e = goI (n+2) es -- 1 for the unimportant sequence, 1 for this important event
+ | otherwise = goU n es
+
+-- | Compresses every sequence of unimportant events down to one event, with
+-- the tag of the first in the sequence.
+compressEvents :: [(a, Event)] -> [(a, Event)]
+compressEvents [] = []
+compressEvents events =
+ let (evsU, evs1) = break (isImportant . snd) events
+ (evsI, evs2) = span (isImportant . snd) evs1
+ tl = evsI ++ compressEvents evs2
+ in case evsU of
+ (tag, e) : ps -> (tag, compress (e : map snd ps)) : tl
+ [] -> tl
+ where
+ compress :: [Event] -> Event
+ compress = Compressed . describe . (`appEndo` CompressedCount 0 0 0 0) . foldMap (Endo . collect)
+
+ collect :: Event -> CompressedCount -> CompressedCount
+ collect Join{} = \cc -> cc { ccJoins = ccJoins cc + 1 }
+ collect Part{} = \cc -> cc { ccLeaves = ccLeaves cc + 1 }
+ collect Quit{} = \cc -> cc { ccLeaves = ccLeaves cc + 1 }
+ collect Mode{} = \cc -> cc { ccModes = ccModes cc + 1 }
+ collect ParseError{} = \cc -> cc { ccParseErrors = ccParseErrors cc + 1 }
+ collect _ = id
+
+ describe :: CompressedCount -> Text
+ describe (CompressedCount nj nl nm np) =
+ T.pack $ intercalate ", " $
+ [show nj ++ " joined" | nj > 0] ++
+ [show nl ++ " left" | nl > 0] ++
+ [show nm ++ " set a mode" | nm > 0] ++
+ [show np ++ " parse errors" | np > 0]
+
+data CompressedCount = CompressedCount
+ { ccJoins :: Int
+ , ccLeaves :: Int
+ , ccModes :: Int
+ , ccParseErrors :: Int }
+
indexUpdateImport :: Index -> Channel -> IO ()
indexUpdateImport index@(Index _ mp _) chan = do
let ciRef = mp Map.! chan
@@ -172,50 +289,55 @@ indexUpdateImport index@(Index _ mp _) chan = do
dayidx = fromIntegral @Integer @Int (day `diffDays` ciStartDay ci)
loadDay index chan ymd >>= \case
- Just (_bs, lineStarts) -> return (Just (dayidx, VS.length lineStarts))
+ Just (bs, lineStarts) ->
+ return (Just (dayidx, Counts (VS.length lineStarts) (countCompressed (map snd (parseLog bs)))))
Nothing -> return Nothing
atomicPrint $ "Update import for " <> prettyChannel chan <> ": " <> T.show readCounts <> " (len = " <> T.show (IGV.length (ciCountUntil ci)) <> ")"
when (not (null readCounts)) $
atomicModifyIORef' ciRef $ \ci' ->
- let ciRes = foldl' (\ci2 (dayidx, count) -> recordCount ci2 dayidx count) ci' readCounts
+ let ciRes = foldl' (\ci2 (dayidx, counts) -> recordCounts ci2 dayidx counts) ci' readCounts
in (ciRes, ())
where
-- | How many events do we already have on this day?
- eventsOnDayIdx :: ChanIndex -> Int -> Int
+ eventsOnDayIdx :: ChanIndex -> Int -> Counts
eventsOnDayIdx ci dayidx
| dayidx == 0 = scan IGV.! 0
| dayidx < IGV.length scan = scan IGV.! dayidx - scan IGV.! (dayidx - 1)
| otherwise = 0
where scan = ciCountUntil ci
- recordCount :: ChanIndex -> Int -> Int -> ChanIndex
- recordCount ci dayidx count
- | count <= alreadyHave = ci
+ recordCounts :: ChanIndex -> Int -> Counts -> ChanIndex
+ recordCounts ci dayidx counts
+ | counts == alreadyHave = ci
| otherwise =
- let ciExt = -- Ensure that there is space in the scan vector for our record
- let nExtraDays = dayidx - IGV.length (ciCountUntil ci) + 1
- lastCount | IGV.length (ciCountUntil ci) == 0 = 0
- | otherwise = ciCountUntil ci IGV.! (IGV.length (ciCountUntil ci) - 1)
+ let ciExt =
+ -- Ensure that there is space in the scan vector for our record
+ -- by copying the last entry a few times
+ let currentLen = IGV.length (ciCountUntil ci)
+ nExtraDays = dayidx - currentLen + 1
+ lastCount | currentLen == 0 = 0
+ | otherwise = ciCountUntil ci IGV.! (currentLen - 1)
in ci { ciCountUntil = iterate (`IGV.append` lastCount) (ciCountUntil ci) !! nExtraDays }
- addFrom di n vec
- | di < IGV.length vec = addFrom (di + 1) n (IGV.set vec di (vec IGV.! di + n))
- | otherwise = vec
- in ciExt { ciCountUntil = addFrom dayidx (count - alreadyHave) (ciCountUntil ciExt)
- , ciTotal = ciTotal ciExt + count - alreadyHave }
+ addFrom di n vec =
+ foldl' (\vec' di' -> IGV.set vec' di' (vec' IGV.! di' + n))
+ vec [di .. IGV.length vec - 1]
+ in ciExt { ciCountUntil = addFrom dayidx (counts - alreadyHave) (ciCountUntil ciExt)
+ , ciTotal = ciTotal ciExt + counts - alreadyHave }
where alreadyHave = eventsOnDayIdx ci dayidx
-- search
-- | Returns proper lazy list of events. Reading the files happens strictly,
-- but parsing the events happens lazily.
-indexGetEventsLinear :: Index -> Channel -> Int -> Int -> IO [(YMDHMS, EventID, Event)]
-indexGetEventsLinear index@(Index _ mp _) chan from count = do
+indexGetEventsLinear :: Index -> Channel -> CountKind -> Int -> Int -> IO [(YMDHMS, EventID, Event)]
+indexGetEventsLinear index@(Index _ mp _) chan kind from count = do
+ atomicPrintS $ "indexGetEventsLinear " ++ show chan ++ " " ++ show kind ++ " " ++ show from ++ " " ++ show count
ci <- readIORef (mp Map.! chan)
- if from + count < 0 || from >= ciTotal ci
+ if from + count < 0 || from >= getCount kind (ciTotal ci)
then return []
- else go ci (ciCountUntil ci)
+ else go ci (IGV.mapUVector (getCountVector kind) (ciCountUntil ci))
where
go ci scan = do
let day1idx = binSearch scan from
@@ -241,34 +363,36 @@ indexGetEventsLinear index@(Index _ mp _) chan from count = do
nOnDay2 = scan IGV.! day2idx - nbefore
in loop (day1idx + 1) (neventBeforeDay1 + neventOnDay1) (neventOnDay1 - off1)
- -- traceM ("ci = " ++ show ci)
- -- traceM ("binSearch " ++ show from ++ " =")
- -- traceM (" " ++ show day1idx)
- -- traceM ("day1 = " ++ show day1)
- -- traceM ("off1 = " ++ show off1)
- -- traceM ("neventOnDay1 = " ++ show neventOnDay1)
- -- traceM ("count = " ++ show count)
- -- traceM ("day2 = " ++ show day2)
- -- traceM ("off2 = " ++ show off2)
+ -- atomicPrintS ("ci = " ++ show ci)
+ -- atomicPrintS ("binSearch " ++ show from ++ " =")
+ -- atomicPrintS (" " ++ show day1idx)
+ -- atomicPrintS ("day1 = " ++ show day1)
+ -- atomicPrintS ("off1 = " ++ show off1)
+ -- atomicPrintS ("neventOnDay1 = " ++ show neventOnDay1)
+ -- atomicPrintS ("count = " ++ show count)
+ -- atomicPrintS ("day2 = " ++ show day2)
+ -- atomicPrintS ("off2 = " ++ show off2)
evs <- forM (zip [day1 .. day2] [day1idx..]) $ \(day, dayidx) ->
- let neventsOnDay | dayidx == 0 = scan IGV.! 0
- | otherwise = scan IGV.! dayidx - scan IGV.! (dayidx - 1)
- range
- | day == day1 =
- if day1 == day2
- then (off1, Just (off2 - off1))
- else (off1, Just (neventsOnDay - off1))
- | day == day2 = (0, Just off2)
- | otherwise = (0, Just neventsOnDay)
- ymd = ymdFromGregorian (toGregorian day)
- in if neventsOnDay > 0
- then loadDay index chan ymd <&> \case
- Just (bs, lineStarts) ->
- let events = parseLogRange range lineStarts bs
- in zipWith (\(hms, ev) off -> (YMDHMS ymd hms, genEventID (YMDHMS ymd hms) off, ev))
- events [fst range ..]
- Nothing -> error $ "events on day " ++ show (dayToYMD day) ++ " but no file"
- else return []
+ let neventsOnDay | dayidx == 0 = scan IGV.! 0
+ | otherwise = scan IGV.! dayidx - scan IGV.! (dayidx - 1)
+ rangeStart = if day == day1 then off1 else 0
+ rangeEnd = if day == day2 then off2 else neventsOnDay
+ range = (rangeStart, Just (rangeEnd - rangeStart))
+ ymd = ymdFromGregorian (toGregorian day)
+ in if neventsOnDay > 0
+ then loadDay index chan ymd <&> \case
+ Just (bs, lineStarts) -> case kind of
+ CKAll ->
+ let events = parseLogRange range lineStarts bs
+ in [(YMDHMS ymd hms, genEventID (YMDHMS ymd hms) off, ev)
+ | ((hms, ev), off) <- zip events [rangeStart ..]]
+ CKCompressed ->
+ let events = parseLog bs
+ events' = take (rangeEnd - rangeStart) $ drop rangeStart $
+ compressEvents [((hms, off), ev) | ((hms, ev), off) <- zip events [0..]]
+ in [(YMDHMS ymd hms, genEventID (YMDHMS ymd hms) off, ev) | ((hms, off), ev) <- events']
+ Nothing -> error $ "events on day " ++ show (dayToYMD day) ++ " but no file"
+ else return []
return (concat evs)
@@ -299,8 +423,8 @@ binSearch vec needle
-- - Timestamp of the event
-- - Index in the linear list of events for this channel
-- - Index in the list of events in the channel on that day
-findEventIDLinear :: Index -> Channel -> EventID -> IO (Maybe (YMDHMS, Int, Int))
-findEventIDLinear index@(Index _ mp _) chan eid = runMaybeT $ do
+findEventIDLinear :: Index -> Channel -> CountKind -> EventID -> IO (Maybe (YMDHMS, Int, Int))
+findEventIDLinear index@(Index _ mp _) chan kind eid = runMaybeT $ do
(ymdhms@(YMDHMS ymd hms), idoff) <- hoistMaybe (parseEventID eid)
day <- hoistMaybe (uncurry3 fromGregorianValid (ymdToGregorian ymd))
@@ -308,24 +432,29 @@ findEventIDLinear index@(Index _ mp _) chan eid = runMaybeT $ do
guard (ciStartDay ci <= day && day <= ciEndDay ci)
(bs, lineStarts) <- MaybeT $ loadDay index chan ymd
- let candidates =
+ let candidates = -- [(event offset, index in possibly compressed event list)]
map snd $
takeWhile ((== hms) . fst) $
dropWhile ((< hms) . fst) $
- zip (parseLogTimesOnly lineStarts bs) [0..]
+ case kind of
+ CKAll -> zip (parseLogTimesOnly lineStarts bs) (zip [0..] [0..])
+ CKCompressed ->
+ let compressed = compressEvents [((hms', off), ev)
+ | ((hms', ev), off) <- zip (parseLog bs) [0..]]
+ in [(hms', (off, idx)) | (((hms', off), _ev), idx) <- zip compressed [0..]]
case candidates of
[] -> empty
_ -> do
let dayidx = fromIntegral @Integer @Int (day `diffDays` ciStartDay ci)
eventsBeforeDay | dayidx == 0 = 0
- | otherwise = ciCountUntil ci IGV.! (dayidx - 1)
- let dayoff = minimumBy (comparing (\off -> abs (off - idoff))) candidates
- return (ymdhms, eventsBeforeDay + dayoff, dayoff)
+ | otherwise = getCount kind (ciCountUntil ci IGV.! (dayidx - 1))
+ let (_dayoff, daylistidx) = minimumBy (comparing (\(off, _listidx) -> abs (off - idoff))) candidates
+ return (ymdhms, eventsBeforeDay + daylistidx, daylistidx)
-- other methods
-indexNumEvents :: Index -> Channel -> IO Int
-indexNumEvents (Index _ mp _) chan = ciTotal <$> readIORef (mp Map.! chan)
+indexNumEvents :: Index -> Channel -> CountKind -> IO Int
+indexNumEvents (Index _ mp _) chan kind = getCount kind . ciTotal <$> readIORef (mp Map.! chan)
indexCalendar :: Index -> Channel -> IO ((Day, Day), [Int])
indexCalendar (Index _ mp _) chan = do
@@ -333,12 +462,12 @@ indexCalendar (Index _ mp _) chan = do
let scan = ciCountUntil ci
return ((ciStartDay ci, ciEndDay ci)
,[if i == 0
- then scan IGV.! 0
- else scan IGV.! i - scan IGV.! (i - 1)
+ then coAll (scan IGV.! 0)
+ else coAll (scan IGV.! i) - coAll (scan IGV.! (i - 1))
| i <- [0 .. IGV.length scan - 1]])
-indexGetEventsDay :: Index -> Channel -> Day -> IO [(HMS, EventID, Event)]
-indexGetEventsDay index@(Index _ mp _) chan day = do
+indexGetEventsDay :: Index -> Channel -> CountKind -> Day -> IO [(HMS, EventID, Event)]
+indexGetEventsDay index@(Index _ mp _) chan kind day = do
ci <- readIORef (mp Map.! chan)
let ymd = dayToYMD day
if day < ciStartDay ci || day > ciEndDay ci
@@ -346,8 +475,14 @@ indexGetEventsDay index@(Index _ mp _) chan day = do
else loadDay index chan ymd <&> \case
Just (bs, _lineStarts) ->
let events = parseLog bs
- in zipWith (\(hms, ev) off -> (hms, genEventID (YMDHMS ymd hms) off, ev))
- events [0..]
+ in case kind of
+ CKAll ->
+ [(hms, genEventID (YMDHMS ymd hms) off, ev)
+ | ((hms, ev), off) <- zip events [0..]]
+ CKCompressed ->
+ [(hms, genEventID (YMDHMS ymd hms) off, ev)
+ | ((hms, off), ev) <- compressEvents [((hms, off), ev)
+ | ((hms, ev), off) <- zip events [0..]]]
Nothing -> [] -- if the file doesn't exist, there ain't no events
-- utilities
@@ -364,6 +499,22 @@ loadDay (Index basedir _ cache) chan@(Channel network channel) ymd = do
Nothing -> return Nothing -- file didn't exist
Just (bs, lineStarts) -> return (Just (bs, lineStarts))
+isImportant :: Event -> Bool
+isImportant ReNick{} = True
+isImportant Talk{} = True
+isImportant Notice{} = True
+isImportant Act{} = True
+isImportant Kick{} = True
+isImportant Topic{} = True
+
+isImportant Join{} = False
+isImportant Part{} = False
+isImportant Quit{} = False
+isImportant Mode{} = False
+isImportant ParseError{} = False
+
+isImportant Compressed{} = error "isImportant: why am I getting a compressed event"
+
-- | Takes the index of the event in the day's log (the "offset") in addition
-- to the timestamp, in order to disambiguate in case there are multiple events
-- with the same timestamp.
diff --git a/src/Main.hs b/src/Main.hs
index 5739065..246b5d3 100644
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -3,6 +3,7 @@
{-# LANGUAGE ViewPatterns #-}
module Main (main) where
+import Control.Applicative ((<|>))
import Control.Monad (guard, forM_)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
@@ -63,12 +64,17 @@ pageLog conf index req alias =
case econfAlias2Chan conf Map.!? alias of
Nothing -> sendText404 "Channel not found"
Just chan -> do
- numEvents <- indexNumEvents index chan
+ let (kind, kindCookie) =
+ case query req "ef" <|> cookie req "ef" of -- event filter
+ Just "all" -> (CKAll, Just "ef=all")
+ Just "compr" -> (CKCompressed, Just "ef=compr")
+ _ -> (CKAll, Nothing)
+ numEvents <- indexNumEvents index chan kind
let npages = (numEvents + numPerPage - 1) `div` numPerPage
(mcurpage, mpagehighlight) <-
if | Just (readMaybe . BS8.unpack -> Just pg) <- query req "page" -> return (Just (min npages (max 1 pg)), Nothing)
| Just (TE.decodeASCII' -> Just eventID) <- query req "eid" -> do
- mevidx <- findEventIDLinear index chan eventID
+ mevidx <- findEventIDLinear index chan kind eventID
case mevidx of
Just (_, evidx, _) -> return (Just (evidx `div` numPerPage + 1), Just (evidx `mod` numPerPage))
Nothing -> return (Nothing, Nothing)
@@ -79,13 +85,17 @@ pageLog conf index req alias =
let ntoleft = min 5 (curpage - 1)
ntoright = min 5 (npages - curpage)
-- traceShowM (indexNumEvents index chan, npages, curpage, ntoleft, ntoright)
- events <- indexGetEventsLinear index chan ((curpage - 1) * numPerPage) numPerPage
- sendHtml200 $
+ events <- indexGetEventsLinear index chan kind ((curpage - 1) * numPerPage) numPerPage
+ let headers = [("Content-Type", "text/html")] ++
+ maybe [] (\s -> [("Set-Cookie", s)]) kindCookie
+ return $ responseBS status200 headers $
renderPageLog LogData
{ network = chanNetwork chan
, channel = chanChannel chan
, alias = alias
, totalevents = renderLargeNumber numEvents
+ , efAll = kind == CKAll
+ , efCompr = kind == CKCompressed
, picker = PickerData
{ prevpage = if curpage > 1 then Just (curpage - 1) else Nothing
, nextpage = if curpage < npages then Just (curpage + 1) else Nothing
@@ -130,10 +140,14 @@ pageCalendarDay conf index req alias datestr =
(Nothing, _) -> sendText404 "Channel not found"
(_, Nothing) -> sendText404 "Invalid date"
(Just chan, Just day) -> do
- events <- indexGetEventsDay index chan day
+ let kind = case query req "ef" of
+ Just "all" -> CKAll
+ Just "compr" -> CKCompressed
+ _ -> CKAll
+ events <- indexGetEventsDay index chan kind day
mpagehighlight <-
if | Just (TE.decodeASCII' -> Just eventID) <- query req "eid" -> do
- mevidx <- findEventIDLinear index chan eventID
+ mevidx <- findEventIDLinear index chan kind eventID
case mevidx of
Just (YMDHMS ymd _, _, evdayidx)
| ymd == ymdFromGregorian (toGregorian day) ->
@@ -184,6 +198,9 @@ classListAdd (Just l) t = l <> " " <> t
query :: Request -> ByteString -> Maybe ByteString
query req key = lookup key (reqQuery req)
+cookie :: Request -> ByteString -> Maybe ByteString
+cookie req name = lookup name (reqCookies req)
+
-- Returns: (classlist, (nickwrap1, nick, nickwrap2), message)
renderEvent :: Event -> (Maybe Text, (Maybe Text, Text, Maybe Text), Text)
renderEvent = \case
@@ -198,6 +215,7 @@ renderEvent = \case
Mode n m -> (j "ev-meta", (no, n, no), "sets mode " <> m)
Topic n m -> (j "ev-meta", (no, n, no), "sets topic to \"" <> m <> "\"")
ParseError -> (j "ev-parseerror", (no, "", no), "<parse error>")
+ Compressed text -> (j "ev-meta", (no, "", no), text)
where no = Nothing; j = Just
pageCalendar :: Config -> Index -> Text -> IO Response
diff --git a/src/Pages.hs b/src/Pages.hs
index 4e85cae..7b114a6 100644
--- a/src/Pages.hs
+++ b/src/Pages.hs
@@ -59,6 +59,8 @@ data LogData = LogData
, channel :: Text
, alias :: Text
, totalevents :: Text
+ , efAll :: Bool
+ , efCompr :: Bool
, picker :: PickerData
, events :: [EventData () Text]
}
diff --git a/src/ZNC.hs b/src/ZNC.hs
index 7cfeb72..502b272 100644
--- a/src/ZNC.hs
+++ b/src/ZNC.hs
@@ -39,6 +39,7 @@ data Event
| Mode Nick Text -- ^ User set mode on the channel.
| Topic Nick Text -- ^ Topic change.
| ParseError
+ | Compressed Text -- ^ Fake event generated when compressing multiple meta events in "Index"
deriving (Show)
-- | Returned vector has one entry for each line in the file, excepting the