Skip to content

Commit

Permalink
Add PhasedUpdates mode
Browse files Browse the repository at this point in the history
In this mode, transactions never have to wait on updates to shared data
base. Instead, a new background thread repeatedly rebuilds the data
bases from change logs, then replaces the data base using synchronized
to assure atomic and coherent replacement. At the start of each
transaction, customer threads use synchronized to obtain the most
current copy of the data bases. This mode allows simulation of very
high allocation rates with very minimal interference from contention
locks between reader and writer threads.
  • Loading branch information
earthling-amzn authored Sep 2, 2022
2 parents 969c776 + 139ed30 commit 00f88fd
Show file tree
Hide file tree
Showing 7 changed files with 959 additions and 50 deletions.
76 changes: 49 additions & 27 deletions Extremem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,55 @@ This produces the Jar file extremem.jar in the src/main/java subdirectory.

Command-line arguments configure Extremem to represent different combinations of workload requirements. Various command-line arguments are described below, in approximate chronological order as to their relevance during execution of each Extremem simulation. In each case, the sample argument display sets the argument to its default value. Though not described explicitly in the descriptions of each argument below, larger data sizes generally result in lower cache locality, which will generally decrease workoad throughput.

### *-dFastAndFurious=false*

In the default Extremem configuration, the shared Customers and Products in-memory databases are each protected by a global
synchronization lock which allows multiple readers and a single writer. Multiple customers can read from these databases
concurrently. Each time a server thread replaces customers or products, a write-lock is required, causing all customer threads
to wait until the server thread has finished its changes to the database. With the high transaction rates required to represent
allocations in excess of 2 GB per second, significant synchronization contention has been observed. This flag changes the
synchronization protocol. The FastAndFurious mode of operation replaces the global multiple-reader-single-writer lock with
a larger number of smaller-context locks. Locks that protect much smaller scopes are held for much shorter
time frames, improving parallel access to shared data structures.
The large majority of these smaller-context locks should normally be uncontended
because the contexts are so small that collisions by multiple threads on the same small contexts is normally
rare. This mode of operation is identified as ``furious'' because it allows false positives and false
negatives. During the process of replacing products, the indexes might report a match to a product that no longer exists.
Likewise, the indexes may not recognize a match for a product that has been newly added but is not yet indexed. This mode
of operation properly uses synchronization to assure coherency of data structures. The default value of the FastAndFurious flag
is false, preserving compatibility with the original Extremem mode of operation. While configuring FastAndFurious=true allows
Extremem to simulate higher allocation rates with less interference from synchronization contention, disabling FastAndFurious
may reveal different weaknesses in particular GC approaches. In particular, interference from synchronization causes allocations
to be more bursty. While a single server thread locks indexes in order to replace products or customers, multiple customer
threads that would normally be allocating are idle, waiting for the server thread to releases its exclusive lock. When the server
thread releases its lock, these customer threads resume execution and allocate at rates much higher than normal because they
have fallen behind their intended execution schedule. This causes a burst of allocation, making it difficult for the GC
scheduling heuristic to predict when the allocation pool will become depleted. If the heuristic is late to trigger the start
of GC, it is likely that the allocation pool will become exhausted before the GC replenishes it, resulting in a degenerated
stop-the-world GC pause.

## *-dPhasedUpdates=false*

In the default Extremem configuration, the shared Customers and Products in-memory databases are each protected by a global
synchronization lock which allows multiple readers and a single writer. Multiple customers can read from these databases
concurrently. Each time a server thread replaces customers or products, a write-lock is required, causing all customer threads
to wait until the server thread has finished its changes to the database. With the high transaction rates required to represent
allocations in excess of 2 GB per second, significant synchronization contention has been observed. This flag changes the
synchronization protocol. The PhasedUpdates mode of operation causes all intended changes to the shared data base to be placed
into a change log. The change log is processed by a single thread running continuously in the background. This thread
copies the existing data base, applies all changes present in the change log, then replaces the old data base with
the new data base. In this mode of operation, the current data base is a read-only data structure requiring no synchronization
for access. A
synchronized method is used to obtain access to the most current version of the shared database. Server threads synchronize
only for the purpose of placing intended changes into the change log. PhasedUpdates and FastAndFurious options are mutually
exclusive. The thread that rebuilds the database does not run if the change log is empty.

## *-dPhasedUpdateInterval=1m*

When PhasedUpdates is true, a dedicated background thread alternates between rebuilding of the Customers and Products
databases. Each time it finishes building a database, it waits PhasedUpdateInterval amount of time before it begins
to rebuild the other database.

### *-dInitializationDelay=50ms*

It is important to complete all initialization of all global data structures before beginning to execute the experimental workload threads. If
Expand Down Expand Up @@ -175,33 +224,6 @@ java -jar src/main/java/extremem.jar \
-dCustomerPeriod=12s -dCustomerThinkTime=8s -dSimulationDuration=20m
```

### *-dFastAndFurious=false*

In the default Extremem configuration, the shared Customers and Products in-memory databases are each protected by a global
synchronization lock which allows multiple readers and a single writer. Multiple customers can read from these databases
concurrently. Each time a server thread replaces customers or products, a write-lock is required, causing all customer threads
to wait until the server thread has finished its changes to the database. With the high transaction rates required to represent
allocations in excess of 2 GB per second, significant synchronization contention has been observed. This flag changes the
synchronization protocol. The FastAndFurious mode of operation replaces the global multiple-reader-single-writer lock with
a larger number of smaller-context locks. Locks that protect much smaller scopes are held for much shorter
time frames, improving parallel access to shared data structures.
The large majority of these smaller-context locks should normally be uncontended
because the contexts are so small that collisions by multiple threads on the same small contexts is normally
rare. This mode of operation is identified as ``furious'' because it allows false positives and false
negatives. During the process of replacing products, the indexes might report a match to a product that no longer exists.
Likewise, the indexes may not recognize a match for a product that has been newly added but is not yet indexed. This mode
of operation properly uses synchronization to assure coherency of data structures. The default value of the FastAndFurious flag
is false, preserving compatibility with the original Extremem mode of operation. While configuring FastAndFurious=true allows
Extremem to simulate higher allocation rates with less interference from synchronization contention, disabling FastAndFurious
may reveal different weaknesses in particular GC approaches. In particular, interference from synchronization causes allocations
to be more bursty. While a single server thread locks indexes in order to replace products or customers, multiple customer
threads that would normally be allocating are idle, waiting for the server thread to releases its exclusive lock. When the server
thread releases its lock, these customer threads resume execution and allocate at rates much higher than normal because they
have fallen behind their intended execution schedule. This causes a burst of allocation, making it difficult for the GC
scheduling heuristic to predict when the allocation pool will become depleted. If the heuristic is late to trigger the start
of GC, it is likely that the allocation pool will become exhausted before the GC replenishes it, resulting in a degenerated
stop-the-world GC pause.

## Interpreting Results

The report displays response times for each of the various distinct operations that are performed by the Extremem workload. The average response times give an approximation of overall performance. A lower average response time corresponds to improved throughput.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class Bootstrap extends ExtrememThread {
}

public void runExtreme() {
UpdateThread update_thread;
CustomerThread[] customer_threads;
ServerThread[] server_threads;

Expand Down Expand Up @@ -269,7 +270,8 @@ public void runExtreme() {
server_threads[i].start(); // will wait for first release
}
staggered_start.garbageFootprint(this);

staggered_start = null;

staggered_customer_replacement.garbageFootprint(this);
staggered_customer_replacement = null;

Expand All @@ -286,7 +288,17 @@ public void runExtreme() {
if (product_replacement_stagger != null)
product_replacement_stagger.garbageFootprint(this);
product_replacement_stagger = null;


if (config.PhasedUpdates()) {
staggered_start = start_time.addRelative(this, config.PhasedUpdateInterval());
update_thread = new UpdateThread(config, randomLong(), all_products, all_customers, staggered_start, end_time);
update_thread.start(); // will wait for first release
staggered_start.garbageFootprint(this);
staggered_start = null;
} else {
update_thread = null;
}

now = AbsoluteTime.now(this);
if (config.ReportCSV()) {
s = Long.toString(now.microseconds());
Expand All @@ -311,7 +323,6 @@ public void runExtreme() {
now = null;

Trace.msg(2, "Joining with customer threads");

// Each thread will terminate when the end_time is reached.
for (int i = 0; i < config.CustomerThreads(); i++) {
try {
Expand All @@ -322,15 +333,27 @@ public void runExtreme() {
}

Trace.msg(2, "Joining with server threads");

for (int i = 0; i < config.ServerThreads(); i++) {
try {
server_threads[i].join();
} catch (InterruptedException x) {
i--; // just try it again
}
}


if (update_thread != null) {
Trace.msg(2, "Joining with update thread");
boolean retry = false;
do {
try {
update_thread.join();
retry = false;
} catch (InterruptedException x) {
retry = true;
}
} while (retry);
}

Trace.msg(2, "Program simulation has ended");
all_products.report(this);
all_customers.report(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Configuration {
static final boolean DefaultReportIndividualThreads = false;
static final boolean DefaultReportCSV = false;
static final boolean DefaultFastAndFurious = false;
static final boolean DefaultPhasedUpdates = false;

static final int DefaultDictionarySize = 25000;
static final String DefaultDictionaryFile = "/usr/share/dict/words";
Expand Down Expand Up @@ -75,6 +76,8 @@ class Configuration {
static final int DefaultProductReplacementPeriodSeconds = 90;
static final int DefaultProductReplacementCount = 64;

static final int DefaultPhasedUpdateIntervalSeconds = 60;

static final long DefaultInitializationDelayMillis = 50;
static final long DefaultDurationMinutes = 10;

Expand All @@ -96,6 +99,7 @@ class Configuration {
private int ServerThreads;

private boolean FastAndFurious;
private boolean PhasedUpdates;;
private boolean ReportIndividualThreads;
private boolean ReportCSV;

Expand All @@ -112,6 +116,8 @@ class Configuration {
private RelativeTime ServerPeriod;
private RelativeTime BrowsingExpiration;

private RelativeTime PhasedUpdateInterval;

// Multiple concurrent Server threads execute with the same period,
// with different stagger values.
private RelativeTime CustomerReplacementPeriod;
Expand Down Expand Up @@ -200,6 +206,7 @@ void initialize(ExtrememThread t) {
RandomSeed = DefaultRandomSeed;

FastAndFurious = DefaultFastAndFurious;
PhasedUpdates = DefaultPhasedUpdates;

SimulationDuration = new RelativeTime(t, DefaultDurationMinutes * 60, 0);
SimulationDuration.changeLifeSpan(t, LifeSpan.NearlyForever);
Expand All @@ -222,6 +229,9 @@ void initialize(ExtrememThread t) {
ServerPeriod = rt.addMillis(t, DefaultServerPeriodMilliseconds);
ServerPeriod.changeLifeSpan(t, LifeSpan.NearlyForever);

PhasedUpdateInterval = rt.addSeconds(t, DefaultPhasedUpdateIntervalSeconds);
PhasedUpdateInterval.changeLifeSpan(t, LifeSpan.NearlyForever);

CustomerReplacementPeriod = (
rt.addSeconds(t, DefaultCustomerReplacementPeriodSeconds));
CustomerReplacementPeriod.changeLifeSpan(t, LifeSpan.NearlyForever);
Expand Down Expand Up @@ -257,6 +267,7 @@ void initialize(ExtrememThread t) {

private static String[] boolean_patterns = {
"FastAndFurious",
"PhasedUpdates",
"ReportCSV",
"ReportIndividualThreads",
};
Expand Down Expand Up @@ -292,6 +303,7 @@ void initialize(ExtrememThread t) {
"CustomerReplacementPeriod",
"CustomerThinkTime",
"InitializationDelay",
"PhasedUpdateInterval",
"ProductReplacementPeriod",
"ServerPeriod",
"SimulationDuration",
Expand Down Expand Up @@ -362,11 +374,16 @@ else if (booleanString.equals("true"))
break;
}
case 1:
if (keyword.equals("PhasedUpdates")) {
PhasedUpdates = b;
break;
}
case 2:
if (keyword.equals("ReportCSV")) {
ReportCSV = b;
break;
}
case 2:
case 3:
if (keyword.equals("ReportIndividualThreads")) {
ReportIndividualThreads = b;
break;
Expand Down Expand Up @@ -585,20 +602,27 @@ else if ((i + 2 == timeString.length()) &&
break;
}
case 5:
if (keyword.equals("PhasedUpdateInterval")) {
PhasedUpdateInterval.garbageFootprint(t);
PhasedUpdateInterval = new RelativeTime(t, secs, nanos);
PhasedUpdateInterval.changeLifeSpan(t, LifeSpan.NearlyForever);
break;
}
case 6:
if (keyword.equals("ProductReplacementPeriod")) {
ProductReplacementPeriod.garbageFootprint(t);
ProductReplacementPeriod = new RelativeTime(t, secs, nanos);
ProductReplacementPeriod.changeLifeSpan(t, LifeSpan.NearlyForever);
break;
}
case 6:
case 7:
if (keyword.equals("ServerPeriod")) {
ServerPeriod.garbageFootprint(t);
ServerPeriod = new RelativeTime(t, secs, nanos);
ServerPeriod.changeLifeSpan(t, LifeSpan.NearlyForever);
break;
}
case 7:
case 8:
if (keyword.equals("SimulationDuration")) {
SimulationDuration.garbageFootprint(t);
SimulationDuration = new RelativeTime(t, secs, nanos);
Expand Down Expand Up @@ -632,6 +656,9 @@ private boolean sufficientVocabulary(int vocab_size, int num_words,
private void assureConfiguration(ExtrememThread t) {
// Ignore memory allocation accounting along early termination paths.

if (PhasedUpdates && FastAndFurious)
usage("Only one of PhasedUpdates or FastAndFurious can be true");

if (DictionarySize < 1)
usage("DictionarySize must be greater or equal to 1");

Expand Down Expand Up @@ -738,6 +765,10 @@ boolean FastAndFurious() {
return FastAndFurious;
}

boolean PhasedUpdates() {
return PhasedUpdates;
}

int MaxArrayLength() {
return MaxArrayLength;
}
Expand Down Expand Up @@ -852,6 +883,10 @@ RelativeTime ProductReplacementPeriod() {
return ProductReplacementPeriod;
}

RelativeTime PhasedUpdateInterval() {
return PhasedUpdateInterval;
}

// Dictionary services
String arbitraryWord(ExtrememThread t) {
return dictionary.arbitraryWord(t);
Expand All @@ -873,9 +908,20 @@ void dumpCSV(ExtrememThread t) {
ReportIndividualThreads? "true": "false");
Report.output("ReportCSV,", ReportCSV? "true": "false");

Report.output();

Report.output("Simulation configuration");

Report.output("FastAndFurious,",
FastAndFurious? "true": "false");
Report.output("PhasedUpdates,",
PhasedUpdates? "true": "false");
Report.output();
s = Long.toString(PhasedUpdateInterval.microseconds());
l = s.length();
Util.ephemeralString(t, l);
Report.output("PhasedUpdateInterval,", s);
Util.abandonEphemeralString(t, l);

s = Integer.toString(RandomSeed);
l = s.length();
Util.ephemeralString(t, l);
Expand Down Expand Up @@ -1081,6 +1127,16 @@ void dump(ExtrememThread t) {
Report.output();
Report.output("Simulation configuration");

Report.output(" Fine-grain locking of data base (FastAndFurious): ", FastAndFurious? "true": "false");
Report.output(" Rebuild data base in phases (PhasedUpdates): ", PhasedUpdates? "true": "false");
Report.output();
s = PhasedUpdateInterval.toString();
l = s.length();
Util.ephemeralString(t, l);
Report.output(" Time between data rebuild (PhasedUpdateInterval): ", s);
Util.abandonEphemeralString(t, l);


s = Integer.toString(RandomSeed);
l = s.length();
Util.ephemeralString(t, l);
Expand Down
Loading

0 comments on commit 00f88fd

Please sign in to comment.