Skip to content

Commit 91d5e8f

Browse files
committed
jepsen.generator: Massive performance improvements
High-concurrency tests (e.g. in the hundreds to thousands of threads) and those which used a lot of reserve/each-thread/clients/nemesis spent a lot of their time manipulating contexts to filter their threads. We tried a few approaches to make this more efficient, with good results, but the cost remained. Likewise, the runtime validator burned a good deal of time performing expensive set computations on contexts. This commit introduces a new, more specialized Context datatype in jepsen.generator.context, supported by jepsen.generator.translation-table. By carefully splitting up parts of the various lookup tables, it makes the fastest changes extremely cheap--and in the case of filtering predicates, memoizable to BitSet intersection, which is very fast indeed. Most generators, including `clients`, `nemesis`, `on-threads`, `reserve`, and `independent/concurrent-generator` now transparently memoize their context-filtering operations. The resulting speedup is dramatic: a realistic generator performance test is now *ten times* faster, pushing 26,000 ops/sec through a 1024-process generator. Context operations are no longer a dominator in the interpreter performance test. Contexts are also deterministic again: they maintain immutable state regarding which process was most recently invoked, and use that to speed up searching for new processes to invoke. This also restores nice error messages for people who try to interact with the old :workers and :free-threads keys in context maps; they'll get a proper error message. This commit may change the ordering of nondeterministic generator tests, but this should be straightforward to adapt to. There should be no significant user-facing changes, other than a massive performance boost.
1 parent c358366 commit 91d5e8f

File tree

9 files changed

+676
-275
lines changed

9 files changed

+676
-275
lines changed

Diff for: jepsen/src/jepsen/generator.clj

+63-34
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,6 @@
397397
context
398398
free-processes
399399
free-threads
400-
next-process
401-
on-threads-context
402400
process->thread
403401
some-free-process
404402
thread->process])
@@ -598,14 +596,14 @@
598596
(not (:process op))
599597
(conj "no :process")
600598

601-
; This is a tad expensive, sadly
602-
(not (.contains ^Set (free-threads ctx)
603-
(process->thread ctx (:process op))))
599+
(not (->> op :process
600+
(context/process->thread ctx)
601+
(context/thread-free? ctx)))
604602
(conj (str "process " (pr-str (:process op))
605603
" is not free"))))))]
606604
(when (seq problems)
607605
(throw+ {:type ::invalid-op
608-
:context ctx
606+
:context (datafy ctx)
609607
;:res res
610608
:problems problems}
611609
nil
@@ -640,30 +638,30 @@
640638
[op (FriendlyExceptions. gen')])
641639
(catch Throwable t
642640
(throw+ {:type ::op-threw
643-
:context ctx}
641+
:context (datafy ctx)}
644642
t
645643
(with-out-str
646644
(print "Generator threw" (class t) "-" (.getMessage t) "when asked for an operation. Generator:\n")
647645
(binding [*print-length* 10]
648646
(pprint gen))
649647
(println "\nContext:\n")
650-
(pprint ctx))))))
648+
(pprint (datafy ctx)))))))
651649

652650
(update [this test ctx event]
653651
(try
654652
(when-let [gen' (update gen test ctx event)]
655653
(FriendlyExceptions. gen'))
656654
(catch Throwable t
657655
(throw+ {:type ::update-threw
658-
:context ctx
656+
:context (datafy ctx)
659657
:event event}
660658
t
661659
(with-out-str
662660
(print "Generator threw " t " when updated with an event. Generator:\n")
663661
(binding [*print-length* 10]
664662
(pprint gen))
665663
(println "\nContext:\n")
666-
(pprint ctx)
664+
(pprint (datafy ctx))
667665
(println "Event:\n")
668666
(pprint event)))))))
669667

@@ -798,15 +796,21 @@
798796
[f gen]
799797
(OnUpdate. f gen))
800798

801-
(defrecord OnThreads [f gen]
799+
(defn on-threads-context
800+
"For backwards compatibility; filters a context to just threads matching (f
801+
thread). Use context/make-thread-filter for performance."
802+
[f context]
803+
((context/make-thread-filter f context) context))
804+
805+
(defrecord OnThreads [f context-filter gen]
802806
Generator
803807
(op [this test ctx]
804-
(when-let [[op gen'] (op gen test (on-threads-context f ctx))]
805-
[op (OnThreads. f gen')]))
808+
(when-let [[op gen'] (op gen test (context-filter ctx))]
809+
[op (OnThreads. f context-filter gen')]))
806810

807811
(update [this test ctx event]
808812
(if (f (process->thread ctx (:process event)))
809-
(OnThreads. f (update gen test (on-threads-context f ctx) event))
813+
(OnThreads. f context-filter (update gen test (context-filter ctx) event))
810814
this)))
811815

812816
(defn on-threads
@@ -815,7 +819,7 @@
815819
generator: it will only include free threads and workers satisfying f.
816820
Updates are passed on only when the thread performing the update matches f."
817821
[f gen]
818-
(OnThreads. f gen))
822+
(OnThreads. f (context/make-thread-filter f) gen))
819823

820824
(def on "For backwards compatibility" on-threads)
821825

@@ -889,26 +893,42 @@
889893
1 (first gens)
890894
(Any. (vec gens))))
891895

892-
(defrecord EachThread [fresh-gen gens]
896+
(defn each-thread-ensure-context-filters!
897+
"Ensures an EachThread has context filters for each thread."
898+
[context-filters ctx]
899+
(when-not (realized? context-filters)
900+
(deliver context-filters
901+
(reduce (fn compute-context-filters [cfs thread]
902+
(assoc cfs thread (context/make-thread-filter
903+
#{thread}
904+
ctx)))
905+
{}
906+
(context/all-threads ctx)))))
907+
908+
(defrecord EachThread [fresh-gen context-filters gens]
893909
; fresh-gen is a generator we use to initialize a thread's state, the first
894910
; time we see it.
911+
; context-filters is a promise of a map of threads to context filters; lazily
912+
; initialized.
895913
; gens is a map of threads to generators.
896914
Generator
897915
(op [this test ctx]
916+
(each-thread-ensure-context-filters! context-filters ctx)
898917
(let [{:keys [op gen' thread] :as soonest}
899918
(->> (context/free-threads ctx)
900919
(keep (fn [thread]
901920
(let [gen (get gens thread fresh-gen)
902921
; Give this generator a context *just* for one
903922
; thread
904-
ctx (on-threads-context #{thread} ctx)]
923+
ctx ((@context-filters thread) ctx)]
905924
(when-let [[op gen'] (op gen test ctx)]
906925
{:op op
907926
:gen' gen'
908927
:thread thread}))))
909928
(reduce soonest-op-map nil))]
910929
(cond ; A free thread has an operation
911-
soonest [op (EachThread. fresh-gen (assoc gens thread gen'))]
930+
soonest [op (EachThread. fresh-gen context-filters
931+
(assoc gens thread gen'))]
912932

913933
; Some thread is busy; we can't tell what to do just yet
914934
(not= (context/free-thread-count ctx)
@@ -920,24 +940,27 @@
920940
nil)))
921941

922942
(update [this test ctx event]
943+
(each-thread-ensure-context-filters! context-filters ctx)
923944
(let [process (:process event)
924945
thread (process->thread ctx process)
925946
gen (get gens thread fresh-gen)
926-
ctx (on-threads-context #{thread} ctx)
947+
ctx ((@context-filters thread) ctx)
927948
gen' (update gen test ctx event)]
928-
(EachThread. fresh-gen (assoc gens thread gen')))))
949+
(EachThread. fresh-gen context-filters (assoc gens thread gen')))))
929950

930951
(defn each-thread
931952
"Takes a generator. Constructs a generator which maintains independent copies
932953
of that generator for every thread. Each generator sees exactly one thread in
933954
its free process list. Updates are propagated to the generator for the thread
934955
which emitted the operation."
935956
[gen]
936-
(EachThread. gen {}))
957+
(EachThread. gen (promise) {}))
937958

938-
(defrecord Reserve [ranges all-ranges gens]
959+
(defrecord Reserve [ranges all-ranges context-filters gens]
939960
; ranges is a collection of sets of threads engaged in each generator.
940961
; all-ranges is the union of all ranges.
962+
; context-filters is a vector of context filtering functions, one for each
963+
; range (and the default gen last).
941964
; gens is a vector of generators corresponding to ranges, followed by the
942965
; default generator.
943966
Generator
@@ -948,17 +971,16 @@
948971
(fn [i threads]
949972
(let [gen (nth gens i)
950973
; Restrict context to this range of threads
951-
ctx (on-threads-context threads ctx)]
974+
ctx ((nth context-filters i) ctx)]
952975
; Ask this range's generator for an op
953976
(when-let [[op gen'] (op gen test ctx)]
954977
; Remember our index
955978
{:op op
956979
:gen' gen'
957980
:weight (count threads)
958981
:i i}))))
959-
; And for the default generator, compute a context without any
960-
; threads from defined ranges...
961-
(cons (let [ctx (on-threads-context (complement all-ranges) ctx)]
982+
; And for the default generator...
983+
(cons (let [ctx ((peek context-filters) ctx)]
962984
; And construct a triple for the default generator
963985
(when-let [[op gen'] (op (peek gens) test ctx)]
964986
(assert ctx)
@@ -969,7 +991,7 @@
969991
(reduce soonest-op-map nil))]
970992
(when soonest
971993
; A range has an operation to do!
972-
[op (Reserve. ranges all-ranges (assoc gens i gen'))])))
994+
[op (Reserve. ranges all-ranges context-filters (assoc gens i gen'))])))
973995

974996
(update [this test ctx event]
975997
(let [process (:process event)
@@ -981,7 +1003,8 @@
9811003
(inc i)))
9821004
0
9831005
ranges)]
984-
(Reserve. ranges all-ranges (c/update gens i update test ctx event)))))
1006+
(Reserve. ranges all-ranges context-filters
1007+
(c/update gens i update test ctx event)))))
9851008

9861009
(defn reserve
9871010
"Takes a series of count, generator pairs, and a final default generator.
@@ -1004,19 +1027,26 @@
10041027
(partition 2)
10051028
; Construct [thread-set gen] tuples defining the range of
10061029
; thread indices covering a given generator, lower
1007-
; inclusive, upper exclusive.
1030+
; inclusive, upper exclusive. TODO: I think there might be a
1031+
; bug here: if we construct nested reserves or otherwise
1032+
; restrict threads, an inner reserve might not understand
1033+
; that its threads don't start at 0.
10081034
(reduce (fn [[n gens] [thread-count gen]]
10091035
(let [n' (+ n thread-count)]
10101036
[n' (conj gens [(set (range n n')) gen])]))
10111037
[0 []])
10121038
second)
10131039
ranges (mapv first gens)
10141040
all-ranges (reduce set/union ranges)
1041+
; Compute context filters for all ranges
1042+
context-filters (mapv context/make-thread-filter
1043+
(c/concat ranges
1044+
[(complement all-ranges)]))
10151045
gens (mapv second gens)
10161046
default (last args)
10171047
gens (conj gens default)]
10181048
(assert default)
1019-
(Reserve. ranges all-ranges gens)))
1049+
(Reserve. ranges all-ranges context-filters gens)))
10201050

10211051
(declare nemesis)
10221052

@@ -1116,9 +1146,7 @@
11161146
(op [_ test ctx]
11171147
(when-not (zero? remaining)
11181148
(when-let [[op gen'] (op gen test ctx)]
1119-
; If you actually hit MIN_INT doing this... you probably have bigger
1120-
; problems on your hands.
1121-
[op (Repeat. (dec remaining) gen)])))
1149+
[op (Repeat. (max -1 (dec remaining)) gen)])))
11221150

11231151
(update [this test ctx event]
11241152
(Repeat. remaining (update gen test ctx event))))
@@ -1333,7 +1361,8 @@
13331361
(defrecord Synchronize [gen]
13341362
Generator
13351363
(op [this test ctx]
1336-
(if (= (context/free-threads ctx) (context/all-threads ctx))
1364+
(if (= (context/free-thread-count ctx)
1365+
(context/all-thread-count ctx))
13371366
; We're ready, replace ourselves with the generator
13381367
(op gen test ctx)
13391368
; Not yet

0 commit comments

Comments
 (0)