New Upstream Release - hdrhistogram

Ready changes

Summary

Merged new upstream version: 2.1.12 (was: 2.1.11).

Resulting package

Built on 2022-12-31T21:36 (took 5m46s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases libhdrhistogram-java

Lintian Result

Diff

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..99780c9
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,23 @@
+name: Java CI
+
+on: [push]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-18.04, macOS-10.14, windows-2016]
+        java: [1.8, 8.0.192, 9, 10, 11, 11.0.3, 12, 12.0.2, 13]
+      fail-fast: false
+      max-parallel: 5           
+    name: Test JDK ${{ matrix.java }}, ${{ matrix.os }}
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Set up JDK
+      uses: actions/setup-java@v1
+      with:
+        java-version: ${{ matrix.java }}
+    - name: Test with Maven
+      run: mvn test -B --file pom.xml
diff --git a/.travis.yml b/.travis.yml
index dff5f3a..0cb08f2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1 +1,5 @@
 language: java
+jdk:
+  - openjdk8
+  - openjdk11
+  - openjdk-ea
diff --git a/GoogleChartsExample/index.html b/GoogleChartsExample/index.html
index e96cc8c..d3be3d4 100644
--- a/GoogleChartsExample/index.html
+++ b/GoogleChartsExample/index.html
@@ -136,7 +136,7 @@
         alert('The File APIs are not fully supported in this browser.');
       }
 
-      // Load the Visualization API and the piechart package.
+      // Load the Visualization API and the corechart package.
       google.load('visualization', '1.0', {'packages':['corechart']});
 
       // Global data series
@@ -170,7 +170,7 @@
 
         var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
 
-        // add tooptips with correct percentile text to data:
+        // add tooltips with correct percentile text to data:
         var columns = [0];
         for (var i = 1; i < data.getNumberOfColumns(); i++) {
           columns.push(i);
@@ -259,16 +259,18 @@
 
             reader.readAsText(f);            
           }
+          return {typed: ''};
         }
 
         function doExport(event) {
           saveSvgAsPng(document.querySelector('svg'), 'Histogram');
+          return {typed: ''};
         }
     </script>
   </head>
 
   <body>
-    <!--Div that will hold the pie chart-->
+    <!--Div that will hold the chart-->
     <div id="chart_div">Please select a histogram file</div>
     <input type='file' onchange='loadFile(event)' multiple>
     <button type='button' onclick='doExport(event)'>Export Image</button>
diff --git a/GoogleChartsExample/plotFiles.html b/GoogleChartsExample/plotFiles.html
index 83c68dd..24044f6 100644
--- a/GoogleChartsExample/plotFiles.html
+++ b/GoogleChartsExample/plotFiles.html
@@ -18,7 +18,7 @@
         alert('The File APIs are not fully supported in this browser.');
     }
 
-    // Load the Visualization API and the piechart package.
+    // Load the Visualization API and the corechart package.
     google.load('visualization', '1.0', {'packages':['corechart']});
 
     // Set a callback to run when the Google Visualization API is loaded.
@@ -96,7 +96,7 @@
             chart = new google.visualization.LineChart(document.getElementById('chart_div'));
         }
 
-        // add tooptips with correct percentile text to data:
+        // add tooltips with correct percentile text to data:
         var columns = [0];
         for (var i = 1; i < chartData.getNumberOfColumns(); i++) {
             columns.push(i);
@@ -174,10 +174,12 @@
 <script>
     function timeUnitsSelected(evt) {
         drawChart();
+        return {typed: ''};
     }
 
     function doExport(event) {
         saveSvgAsPng(document.querySelector('svg'), 'Histogram', 2.0);
+        return {typed: ''};
     }
 </script>
 
@@ -255,8 +257,8 @@
             var css = "";
             var sheets = document.styleSheets;
             for (var i = 0; i < sheets.length; i++) {
-                var rules = sheets[i].cssRules;
-                if (rules != null) {
+                if (sheets[i].hasOwnProperty('cssRules')) {
+                    var rules = sheets[i].cssRules;
                     for (var j = 0; j < rules.length; j++) {
                         var rule = rules[j];
                         if (typeof(rule.style) != "undefined") {
@@ -350,7 +352,7 @@
 
 <pre id="fileDisplayArea">Please select file(s) above.</pre>
 
-<!--Div that will hold the pie chart-->
+<!--Div that will hold the chart-->
 <div id="chart_div">None Loaded</div>
 
 Latency time units:
@@ -378,6 +380,7 @@ Percentile range:
         document.getElementById("percentileRange").innerHTML=percentile + "%";
         maxPercentile = x;
         drawChart();
+        return {typed: ''};
     }
 </script>
 </p>
diff --git a/README.md b/README.md
index d2d9e5b..f67d8a7 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
 HdrHistogram
 ----------------------------------------------
 [![Gitter](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/HdrHistogram/HdrHistogram?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-[![Build Status](https://travis-ci.org/HdrHistogram/HdrHistogram.svg?branch=master)](https://travis-ci.org/HdrHistogram/HdrHistogram) 
+[![Build Status](https://travis-ci.org/HdrHistogram/HdrHistogram.svg?branch=master)](https://travis-ci.org/HdrHistogram/HdrHistogram)
+[![Java CI](https://github.com/hdrhistogram/hdrhistogram/workflows/Java%20CI/badge.svg)](https://github.com/hdrhistogram/hdrhistogram/actions)
 [![Javadocs](http://www.javadoc.io/badge/org.hdrhistogram/HdrHistogram.svg)](http://www.javadoc.io/doc/org.hdrhistogram/HdrHistogram)
 [![Total alerts](https://img.shields.io/lgtm/alerts/g/HdrHistogram/HdrHistogram.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/HdrHistogram/HdrHistogram/alerts/)
 [![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/HdrHistogram/HdrHistogram.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/HdrHistogram/HdrHistogram/context:java)
@@ -9,9 +10,9 @@ HdrHistogram
 ----------------------------------------------------------------------------
 HdrHistogram: A High Dynamic Range (HDR) Histogram
 
-This respository currently includes a Java implementation of
+This repository currently includes a Java implementation of
 HdrHistogram. C, C#/.NET, Python, Javascript, Rust, Erlang, and Go ports
-can be found in other respositories. All of which share common concepts
+can be found in other repositories. All of which share common concepts
 and data representation capabilities. Look at repositories under the
 [HdrHistogram organization](https://github.com/HdrHistogram) for various
 implementations and useful tools.
@@ -20,7 +21,7 @@ Note: The below is an excerpt from a Histogram JavaDoc. While much
 of it generally applies to other language implementations as well,
 some details may vary by implementation (e.g. iteration and
 synchronization), so you should consult the documentation or header
-information of specific API library you intended to use.
+information of the specific API library you intend to use.
 
 ----------------------------------------------
 
@@ -71,7 +72,7 @@ low [and ultimately configurable] loss in accuracy when compared to
 performing the same analysis directly on the potentially infinite series of
 sourced data values samples.
 
-An common use example of HdrHistogram would be to record response times
+A common use example of HdrHistogram would be to record response times
 in units of microseconds across a dynamic range stretching from 1 usec to
 over an hour, with a good enough resolution to support later performing
 post-recording analysis on the collected data. Analysis can include
@@ -136,12 +137,12 @@ sampling, analyzing, or reporting operations are needed, consider using
 the Recorder and SingleWriterRecorder recorder variants that were specifically
 designed for that purpose. Recorders provide a recording API similar to
 Histogram, and internally maintain and coordinate active/inactive histograms
-such that recording remains wait-free in the presense of accurate and stable
+such that recording remains wait-free in the presence of accurate and stable
 interval sampling.
 
 It is worth mentioning that since Histogram objects are additive, it is
 common practice to use per-thread non-synchronized histograms or
-SingleWriterRecorders, and using a summary/reporting thread perform
+SingleWriterRecorders, and using a summary/reporting thread to perform
 histogram aggregation math across time and/or threads.  
 
 
@@ -268,13 +269,13 @@ random, uncoordinated request would have experienced.
 Footprint estimation
 ----------------------------------------------
 
-Due to it's dynamic range representation, Histogram is relatively efficient
+Due to its dynamic range representation, Histogram is relatively efficient
 in memory space requirements given the accuracy and dynamic range it covers.
 Still, it is useful to be able to estimate the memory footprint involved
 for a given highestTrackableValue and numberOfSignificantValueDigits
 combination. Beyond a relatively small fixed-size footprint used for internal
 fields and stats (which can be estimated as "fixed at well less than 1KB"),
-the bulk of a Histogram's storage is taken up by it's data value recording
+the bulk of a Histogram's storage is taken up by its data value recording
 counts array. The total footprint can be conservatively estimated by:
 
 ``` java
diff --git a/debian/changelog b/debian/changelog
index a7316eb..d97ea48 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+hdrhistogram (2.1.12-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 31 Dec 2022 21:31:11 -0000
+
 hdrhistogram (2.1.11-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/pom.xml b/pom.xml
index 6f33b77..6bb9e4b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     <groupId>org.hdrhistogram</groupId>
     <artifactId>HdrHistogram</artifactId>
-    <version>2.1.11</version>
+    <version>2.1.12</version>
 
     <name>HdrHistogram</name>
 
@@ -46,7 +46,7 @@
         <url>scm:git:git://github.com/HdrHistogram/HdrHistogram.git</url>
         <connection>scm:git:git://github.com/HdrHistogram/HdrHistogram.git</connection>
         <developerConnection>scm:git:git@github.com:HdrHistogram/HdrHistogram.git</developerConnection>
-      <tag>HdrHistogram-2.1.11</tag>
+      <tag>HdrHistogram-2.1.12</tag>
     </scm>
 
 
@@ -61,6 +61,10 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <version.template.file>src/main/java/org/HdrHistogram/Version.java.template</version.template.file>
         <version.file>src/main/java/org/HdrHistogram/Version.java</version.file>
+
+        <junit.version>4.12</junit.version>
+        <junit.jupiter.version>5.5.2</junit.jupiter.version>
+        <junit.vintage.version>5.5.2</junit.vintage.version>
     </properties>
 
     <distributionManagement>
@@ -123,15 +127,15 @@
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>3.8.0</version>
                 <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
+                    <source>1.7</source>
+                    <target>1.7</target>
                     <encoding>UTF-8</encoding>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.12.4</version>
+                <version>3.0.0-M3</version>
                 <configuration>
                     <enableAssertions>false</enableAssertions>
                 </configuration>
@@ -200,6 +204,20 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-gpg-plugin</artifactId>
+                <version>1.5</version>
+                <executions>
+                    <execution>
+                        <id>sign-artifacts</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>sign</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>org.sonatype.plugins</groupId>
                 <artifactId>nexus-staging-maven-plugin</artifactId>
@@ -214,62 +232,35 @@
         </plugins>
     </build>
 
-    <profiles>
-        <profile>
-            <id>deploy</id>
-            <build>
-                <plugins>
-                    <plugin>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-gpg-plugin</artifactId>
-                        <version>1.5</version>
-                        <executions>
-                            <execution>
-                                <id>sign-artifacts</id>
-                                <phase>verify</phase>
-                                <goals>
-                                    <goal>sign</goal>
-                                </goals>
-                            </execution>
-                        </executions>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-        <profile>
-            <id>release</id>
-            <activation>
-                <property>
-                    <name>performRelease</name>
-                    <value>true</value>
-                </property>
-            </activation>
-            <build>
-                <plugins>
-                    <plugin>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-gpg-plugin</artifactId>
-                        <version>1.5</version>
-                        <executions>
-                            <execution>
-                                <id>sign-artifacts</id>
-                                <phase>verify</phase>
-                                <goals>
-                                    <goal>sign</goal>
-                                </goals>
-                            </execution>
-                        </executions>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-    </profiles>
-
     <dependencies>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.10</version>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.vintage</groupId>
+            <artifactId>junit-vintage-engine</artifactId>
+            <version>${junit.vintage.version}</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/src/main/java/org/HdrHistogram/AbstractHistogram.java b/src/main/java/org/HdrHistogram/AbstractHistogram.java
index ef5a102..fb181d2 100644
--- a/src/main/java/org/HdrHistogram/AbstractHistogram.java
+++ b/src/main/java/org/HdrHistogram/AbstractHistogram.java
@@ -133,13 +133,24 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
     // Sub-classes will typically add a totalCount field and a counts array field, which will likely be laid out
     // right around here due to the subclass layout rules in most practical JVM implementations.
 
-    //
-    //
+    //   ########     ###     ######  ##    ##    ###     ######   ########
+    //   ##     ##   ## ##   ##    ## ##   ##    ## ##   ##    ##  ##
+    //   ##     ##  ##   ##  ##       ##  ##    ##   ##  ##        ##
+    //   ########  ##     ## ##       #####    ##     ## ##   #### ######
+    //   ##        ######### ##       ##  ##   ######### ##    ##  ##
+    //   ##        ##     ## ##    ## ##   ##  ##     ## ##    ##  ##
+    //   ##        ##     ##  ######  ##    ## ##     ##  ######   ########
+    //
+    //      ###    ########   ######  ######## ########     ###     ######  ########
+    //     ## ##   ##     ## ##    ##    ##    ##     ##   ## ##   ##    ##    ##
+    //    ##   ##  ##     ## ##          ##    ##     ##  ##   ##  ##          ##
+    //   ##     ## ########   ######     ##    ########  ##     ## ##          ##
+    //   ######### ##     ##       ##    ##    ##   ##   ######### ##          ##
+    //   ##     ## ##     ## ##    ##    ##    ##    ##  ##     ## ##    ##    ##
+    //   ##     ## ########   ######     ##    ##     ## ##     ##  ######     ##
     //
     // Abstract, counts-type dependent methods to be provided by subclass implementations:
     //
-    //
-    //
 
     abstract long getCountAtIndex(int index);
 
@@ -217,13 +228,16 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
                 minNonZeroValue : internalValue;
     }
 
-    //
-    //
+    //    ######   #######  ##    ##  ######  ######## ########  ##     ##  ######  ######## ####  #######  ##    ##
+    //   ##    ## ##     ## ###   ## ##    ##    ##    ##     ## ##     ## ##    ##    ##     ##  ##     ## ###   ##
+    //   ##       ##     ## ####  ## ##          ##    ##     ## ##     ## ##          ##     ##  ##     ## ####  ##
+    //   ##       ##     ## ## ## ##  ######     ##    ########  ##     ## ##          ##     ##  ##     ## ## ## ##
+    //   ##       ##     ## ##  ####       ##    ##    ##   ##   ##     ## ##          ##     ##  ##     ## ##  ####
+    //   ##    ## ##     ## ##   ### ##    ##    ##    ##    ##  ##     ## ##    ##    ##     ##  ##     ## ##   ###
+    //    ######   #######  ##    ##  ######     ##    ##     ##  #######   ######     ##    ####  #######  ##    ##
     //
     // Construction:
     //
-    //
-    //
 
     /**
      * Construct an auto-resizing histogram with a lowest discernible value of 1 and an auto-adjusting
@@ -288,7 +302,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         this.autoResize = source.autoResize;
     }
 
-    @SuppressWarnings("deprecation")
     private void init(final long lowestDiscernibleValue,
                       final long highestTrackableValue,
                       final int numberOfSignificantValueDigits,
@@ -377,11 +390,24 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         return countsArrayLength;
     }
 
+    //      ###    ##     ## ########  #######
+    //     ## ##   ##     ##    ##    ##     ##
+    //    ##   ##  ##     ##    ##    ##     ##
+    //   ##     ## ##     ##    ##    ##     ##
+    //   ######### ##     ##    ##    ##     ##
+    //   ##     ## ##     ##    ##    ##     ##
+    //   ##     ##  #######     ##     #######
     //
+    //   ########  ########  ######  #### ######## #### ##    ##  ######
+    //   ##     ## ##       ##    ##  ##       ##   ##  ###   ## ##    ##
+    //   ##     ## ##       ##        ##      ##    ##  ####  ## ##
+    //   ########  ######    ######   ##     ##     ##  ## ## ## ##   ####
+    //   ##   ##   ##             ##  ##    ##      ##  ##  #### ##    ##
+    //   ##    ##  ##       ##    ##  ##   ##       ##  ##   ### ##    ##
+    //   ##     ## ########  ######  #### ######## #### ##    ##  ######
     //
     // Auto-resizing control:
     //
-    //
 
     /**
      * Indicate whether or not the histogram is set to auto-resize and auto-adjust it's
@@ -410,13 +436,25 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         this.autoResize = autoResize;
     }
 
+    //   ##     ##    ###    ##       ##     ## ########
+    //   ##     ##   ## ##   ##       ##     ## ##
+    //   ##     ##  ##   ##  ##       ##     ## ##
+    //   ##     ## ##     ## ##       ##     ## ######
+    //    ##   ##  ######### ##       ##     ## ##
+    //     ## ##   ##     ## ##       ##     ## ##
+    //      ###    ##     ## ########  #######  ########
     //
-    //
+    //   ########  ########  ######   #######  ########  ########  #### ##    ##  ######
+    //   ##     ## ##       ##    ## ##     ## ##     ## ##     ##  ##  ###   ## ##    ##
+    //   ##     ## ##       ##       ##     ## ##     ## ##     ##  ##  ####  ## ##
+    //   ########  ######   ##       ##     ## ########  ##     ##  ##  ## ## ## ##   ####
+    //   ##   ##   ##       ##       ##     ## ##   ##   ##     ##  ##  ##  #### ##    ##
+    //   ##    ##  ##       ##    ## ##     ## ##    ##  ##     ##  ##  ##   ### ##    ##
+    //   ##     ## ########  ######   #######  ##     ## ########  #### ##    ##  ######
     //
     // Value recording support:
     //
-    //
-    //
+
 
     /**
      * Record a value in the histogram
@@ -510,8 +548,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         int countsIndex = countsArrayIndex(value);
         try {
             addToCountAtIndex(countsIndex, count);
-        } catch (ArrayIndexOutOfBoundsException ex) {
-            handleRecordException(count, value, ex);
         } catch (IndexOutOfBoundsException ex) {
             handleRecordException(count, value, ex);
         }
@@ -523,8 +559,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         int countsIndex = countsArrayIndex(value);
         try {
             incrementCountAtIndex(countsIndex);
-        } catch (ArrayIndexOutOfBoundsException ex) {
-            handleRecordException(1, value, ex);
         } catch (IndexOutOfBoundsException ex) {
             handleRecordException(1, value, ex);
         }
@@ -568,13 +602,17 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         }
     }
 
-    //
-    //
+    //    ######  ##       ########    ###    ########  #### ##    ##  ######
+    //   ##    ## ##       ##         ## ##   ##     ##  ##  ###   ## ##    ##
+    //   ##       ##       ##        ##   ##  ##     ##  ##  ####  ## ##
+    //   ##       ##       ######   ##     ## ########   ##  ## ## ## ##   ####
+    //   ##       ##       ##       ######### ##   ##    ##  ##  #### ##    ##
+    //   ##    ## ##       ##       ##     ## ##    ##   ##  ##   ### ##    ##
+    //    ######  ######## ######## ##     ## ##     ## #### ##    ##  ######
     //
     // Clearing support:
     //
-    //
-    //
+
 
     /**
      * Reset the contents and stats of this histogram
@@ -590,13 +628,17 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         tag = null;
     }
 
-    //
-    //
+    //     ######   #######  ########  ##    ##
+    //   ##    ## ##     ## ##     ##  ##  ##
+    //   ##       ##     ## ##     ##   ####
+    //   ##       ##     ## ########     ##    
+    //   ##       ##     ## ##           ##
+    //   ##    ## ##     ## ##           ##
+    //    ######   #######  ##           ##
     //
     // Copy support:
     //
-    //
-    //
+
 
     /**
      * Create a copy of this histogram, complete with data and everything.
@@ -658,13 +700,17 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         targetHistogram.setEndTimeStamp(this.endTimeStampMsec);
     }
 
-    //
-    //
+    //      ###    ########  ########
+    //     ## ##   ##     ## ##     ##
+    //    ##   ##  ##     ## ##     ##
+    //   ##     ## ##     ## ##     ##
+    //   ######### ##     ## ##     ##
+    //   ##     ## ##     ## ##     ##
+    //   ##     ## ########  ########
     //
     // Add support:
     //
-    //
-    //
+
 
     /**
      * Add the contents of another histogram to this one.
@@ -790,13 +836,18 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         }
     }
 
-    //
+    //    ######  ##     ## #### ######## ######## #### ##    ##  ######
+    //   ##    ## ##     ##  ##  ##          ##     ##  ###   ## ##    ##
+    //   ##       ##     ##  ##  ##          ##     ##  ####  ## ##
+    //    ######  #########  ##  ######      ##     ##  ## ## ## ##   ####
+    //         ## ##     ##  ##  ##          ##     ##  ##  #### ##    ##
+    //   ##    ## ##     ##  ##  ##          ##     ##  ##   ### ##    ##
+    //    ######  ##     ## #### ##          ##    #### ##    ##  ######
     //
     //
     // Shifting support:
     //
-    //
-    //
+
 
     /**
      * Shift recorded values to the left (the equivalent of a &lt;&lt; shift operation on all recorded values). The
@@ -817,7 +868,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         if (numberOfBinaryOrdersOfMagnitude < 0) {
             throw new IllegalArgumentException("Cannot shift by a negative number of magnitudes");
         }
-
         if (numberOfBinaryOrdersOfMagnitude == 0) {
             return;
         }
@@ -928,7 +978,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         if (numberOfBinaryOrdersOfMagnitude < 0) {
             throw new IllegalArgumentException("Cannot shift by a negative number of magnitudes");
         }
-
         if (numberOfBinaryOrdersOfMagnitude == 0) {
             return;
         }
@@ -986,13 +1035,17 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         }
     }
 
-    //
-    //
+    //    ######   #######  ##     ## ########     ###    ########  ####  ######   #######  ##    ##
+    //   ##    ## ##     ## ###   ### ##     ##   ## ##   ##     ##  ##  ##    ## ##     ## ###   ##
+    //   ##       ##     ## #### #### ##     ##  ##   ##  ##     ##  ##  ##       ##     ## ####  ##
+    //   ##       ##     ## ## ### ## ########  ##     ## ########   ##   ######  ##     ## ## ## ##
+    //   ##       ##     ## ##     ## ##        ######### ##   ##    ##        ## ##     ## ##  ####
+    //   ##    ## ##     ## ##     ## ##        ##     ## ##    ##   ##  ##    ## ##     ## ##   ###
+    //    ######   #######  ##     ## ##        ##     ## ##     ## ####  ######   #######  ##    ##
     //
     // Comparison support:
     //
-    //
-    //
+
 
     /**
      * Determine if this histogram is equivalent to another.
@@ -1067,13 +1120,24 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         return h;
     }
 
+    //    ######  ######## ########  ##     ##  ######  ######## ##     ## ########  ########
+    //   ##    ##    ##    ##     ## ##     ## ##    ##    ##    ##     ## ##     ## ##
+    //   ##          ##    ##     ## ##     ## ##          ##    ##     ## ##     ## ##
+    //    ######     ##    ########  ##     ## ##          ##    ##     ## ########  ######
+    //         ##    ##    ##   ##   ##     ## ##          ##    ##     ## ##   ##   ##
+    //   ##    ##    ##    ##    ##  ##     ## ##    ##    ##    ##     ## ##    ##  ##
+    //    ######     ##    ##     ##  #######   ######     ##     #######  ##     ## ######## 
     //
-    //
+    //    #######  ##     ## ######## ########  ##    ## #### ##    ##  ######
+    //   ##     ## ##     ## ##       ##     ##  ##  ##   ##  ###   ## ##    ##
+    //   ##     ## ##     ## ##       ##     ##   ####    ##  ####  ## ##
+    //   ##     ## ##     ## ######   ########     ##     ##  ## ## ## ##   ####
+    //   ##  ## ## ##     ## ##       ##   ##      ##     ##  ##  #### ##    ##
+    //   ##    ##  ##     ## ##       ##    ##     ##     ##  ##   ### ##    ##
+    //    ##### ##  #######  ######## ##     ##    ##    #### ##    ##  ######
     //
     // Histogram structure querying support:
     //
-    //
-    //
 
     /**
      * get the configured lowestDiscernibleValue
@@ -1109,7 +1173,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
      */
     public long sizeOfEquivalentValueRange(final long value) {
         final int bucketIndex = getBucketIndex(value);
-        final int subBucketIndex = getSubBucketIndex(value, bucketIndex);
         long distanceToNextValue = 1L << (unitMagnitude + bucketIndex);
         return distanceToNextValue;
     }
@@ -1187,13 +1250,24 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         return _getEstimatedFootprintInBytes();
     }
 
+    //   ######## #### ##     ## ########  ######  ########    ###    ##     ## ########
+    //      ##     ##  ###   ### ##       ##    ##    ##      ## ##   ###   ### ##     ##
+    //      ##     ##  #### #### ##       ##          ##     ##   ##  #### #### ##     ##
+    //      ##     ##  ## ### ## ######    ######     ##    ##     ## ## ### ## ########
+    //      ##     ##  ##     ## ##             ##    ##    ######### ##     ## ##
+    //      ##     ##  ##     ## ##       ##    ##    ##    ##     ## ##     ## ##
+    //      ##    #### ##     ## ########  ######     ##    ##     ## ##     ## ##
     //
-    //
+    //     ####       ########    ###     ######
+    //    ##  ##         ##      ## ##   ##    ##
+    //     ####          ##     ##   ##  ##
+    //    ####           ##    ##     ## ##   ####
+    //   ##  ## ##       ##    ######### ##    ##  
+    //   ##   ##         ##    ##     ## ##    ##
+    //    ####  ##       ##    ##     ##  ######
     //
     // Timestamp and tag support:
     //
-    //
-    //
 
     /**
      * get the start time stamp [optionally] stored with this histogram
@@ -1247,13 +1321,17 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         this.tag = tag;
     }
 
-    //
-    //
+    //   ########     ###    ########    ###          ###     ######   ######  ########  ######   ######
+    //   ##     ##   ## ##      ##      ## ##        ## ##   ##    ## ##    ## ##       ##    ## ##    ##
+    //   ##     ##  ##   ##     ##     ##   ##      ##   ##  ##       ##       ##       ##       ##
+    //   ##     ## ##     ##    ##    ##     ##    ##     ## ##       ##       ######    ######   ######
+    //   ##     ## #########    ##    #########    ######### ##       ##       ##             ##       ##
+    //   ##     ## ##     ##    ##    ##     ##    ##     ## ##    ## ##    ## ##       ##    ## ##    ##
+    //   ########  ##     ##    ##    ##     ##    ##     ##  ######   ######  ########  ######   ######
     //
     // Histogram Data access support:
     //
-    //
-    //
+
 
     /**
      * Get the lowest recorded value level in the histogram. If the histogram has no recorded values,
@@ -1432,6 +1510,16 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         return getCountAtIndex(index);
     }
 
+    //   #### ######## ######## ########     ###    ######## ####  #######  ##    ##
+    //    ##     ##    ##       ##     ##   ## ##      ##     ##  ##     ## ###   ##
+    //    ##     ##    ##       ##     ##  ##   ##     ##     ##  ##     ## ####  ##
+    //    ##     ##    ######   ########  ##     ##    ##     ##  ##     ## ## ## ##
+    //    ##     ##    ##       ##   ##   #########    ##     ##  ##     ## ##  ####
+    //    ##     ##    ##       ##    ##  ##     ##    ##     ##  ##     ## ##   ###
+    //   ####    ##    ######## ##     ## ##     ##    ##    ####  #######  ##    ##
+    //
+    // Iteration Support:
+    //
     /**
      * Provide a means of iterating through histogram values according to percentile levels. The iteration is
      * performed in steps that start at 0% and reduce their distance to 100% according to the
@@ -1521,6 +1609,7 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         /**
          * @return A {@link PercentileIterator}{@literal <}{@link HistogramIterationValue}{@literal >}
          */
+        @Override
         public Iterator<HistogramIterationValue> iterator() {
             return new PercentileIterator(histogram, percentileTicksPerHalfDistance);
         }
@@ -1617,6 +1706,24 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         }
     }
 
+    //   ########  ######## ########   ######  ######## ##    ## ######## #### ##       ########
+    //   ##     ## ##       ##     ## ##    ## ##       ###   ##    ##     ##  ##       ##
+    //   ##     ## ##       ##     ## ##       ##       ####  ##    ##     ##  ##       ##
+    //   ########  ######   ########  ##       ######   ## ## ##    ##     ##  ##       ######
+    //   ##        ##       ##   ##   ##       ##       ##  ####    ##     ##  ##       ##
+    //   ##        ##       ##    ##  ##    ## ##       ##   ###    ##     ##  ##       ##
+    //   ##        ######## ##     ##  ######  ######## ##    ##    ##    #### ######## ########
+    //
+    //    #######  ##     ## ######## ########  ##     ## ########
+    //   ##     ## ##     ##    ##    ##     ## ##     ##    ##
+    //   ##     ## ##     ##    ##    ##     ## ##     ##    ##
+    //   ##     ## ##     ##    ##    ########  ##     ##    ##
+    //   ##     ## ##     ##    ##    ##        ##     ##    ##
+    //   ##     ## ##     ##    ##    ##        ##     ##    ##
+    //    #######   #######     ##    ##         #######     ##
+    //
+    // Textual percentile output support:
+    //
 
     /**
      * Produce textual representation of the value distribution of histogram data by percentile. The distribution is
@@ -1633,14 +1740,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         outputPercentileDistribution(printStream, 5, outputValueUnitScalingRatio);
     }
 
-    //
-    //
-    //
-    // Textual percentile output support:
-    //
-    //
-    //
-
     /**
      * Produce textual representation of the value distribution of histogram data by percentile. The distribution is
      * output with exponentially increasing resolution, with each exponentially decreasing half-distance containing
@@ -1739,13 +1838,16 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         }
     }
 
-    //
-    //
+    //    ######  ######## ########  ####    ###    ##       #### ########    ###    ######## ####  #######  ##    ##
+    //   ##    ## ##       ##     ##  ##    ## ##   ##        ##       ##    ## ##      ##     ##  ##     ## ###   ##
+    //   ##       ##       ##     ##  ##   ##   ##  ##        ##      ##    ##   ##     ##     ##  ##     ## ####  ##
+    //    ######  ######   ########   ##  ##     ## ##        ##     ##    ##     ##    ##     ##  ##     ## ## ## ##
+    //         ## ##       ##   ##    ##  ######### ##        ##    ##     #########    ##     ##  ##     ## ##  ####
+    //   ##    ## ##       ##    ##   ##  ##     ## ##        ##   ##      ##     ##    ##     ##  ##     ## ##   ###
+    //    ######  ######## ##     ## #### ##     ## ######## #### ######## ##     ##    ##    ####  #######  ##    ##
     //
     // Serialization support:
     //
-    //
-    //
 
     private static final long serialVersionUID = 0x1c849302;
 
@@ -1796,13 +1898,24 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         wordSizeInBytes = indicatedwordSizeInBytes;
     }
 
+    //   ######## ##    ##  ######   #######  ########  #### ##    ##  ######
+    //   ##       ###   ## ##    ## ##     ## ##     ##  ##  ###   ## ##    ##
+    //   ##       ####  ## ##       ##     ## ##     ##  ##  ####  ## ##
+    //   ######   ## ## ## ##       ##     ## ##     ##  ##  ## ## ## ##   ####
+    //   ##       ##  #### ##       ##     ## ##     ##  ##  ##  #### ##    ##
+    //   ##       ##   ### ##    ## ##     ## ##     ##  ##  ##   ### ##    ##
+    //   ######## ##    ##  ######   #######  ########  #### ##    ##  ######   
     //
-    //
+    //     ####       ########  ########  ######   #######  ########  #### ##    ##  ######
+    //    ##  ##      ##     ## ##       ##    ## ##     ## ##     ##  ##  ###   ## ##    ##
+    //     ####       ##     ## ##       ##       ##     ## ##     ##  ##  ####  ## ##
+    //    ####        ##     ## ######   ##       ##     ## ##     ##  ##  ## ## ## ##   ####
+    //   ##  ## ##    ##     ## ##       ##       ##     ## ##     ##  ##  ##  #### ##    ##
+    //   ##   ##      ##     ## ##       ##    ## ##     ## ##     ##  ##  ##   ### ##    ##
+    //    ####  ##    ########  ########  ######   #######  ########  #### ##    ##  ######
     //
     // Encoding/Decoding support:
     //
-    //
-    //
 
     /**
      * Get the capacity needed to encode this histogram into a ByteBuffer
@@ -1828,8 +1941,6 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         return (relevantLength * wordSizeInBytes);
     }
 
-    abstract void fillCountsArrayFromBuffer(ByteBuffer buffer, int length);
-
     private static final int V0EncodingCookieBase = 0x1c849308;
     private static final int V0CompressedEncodingCookieBase = 0x1c849309;
 
@@ -2018,19 +2129,18 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
 
         // Construct histogram:
         try {
-            @SuppressWarnings("unchecked")
             Constructor<T> constructor = histogramClass.getConstructor(constructorArgsTypes);
             histogram = constructor.newInstance(lowestTrackableUnitValue, highestTrackableValue,
                     numberOfSignificantValueDigits);
             histogram.setIntegerToDoubleValueConversionRatio(integerToDoubleValueConversionRatio);
             histogram.setNormalizingIndexOffset(normalizingIndexOffset);
-        } catch (IllegalAccessException ex) {
-            throw new IllegalArgumentException(ex);
-        } catch (NoSuchMethodException ex) {
-            throw new IllegalArgumentException(ex);
-        } catch (InstantiationException ex) {
-            throw new IllegalArgumentException(ex);
-        } catch (InvocationTargetException ex) {
+            try {
+                histogram.setAutoResize(true);
+            } catch (IllegalStateException ex) {
+                // Allow histogram to refuse auto-sizing setting
+            }
+        } catch (IllegalAccessException | NoSuchMethodException |
+                InstantiationException | InvocationTargetException ex) {
             throw new IllegalArgumentException(ex);
         }
 
@@ -2188,13 +2298,47 @@ public abstract class AbstractHistogram extends AbstractHistogramBase implements
         return histogram;
     }
 
+    //   #### ##    ## ######## ######## ########  ##    ##    ###    ##
+    //    ##  ###   ##    ##    ##       ##     ## ###   ##   ## ##   ##
+    //    ##  ####  ##    ##    ##       ##     ## ####  ##  ##   ##  ##
+    //    ##  ## ## ##    ##    ######   ########  ## ## ## ##     ## ##
+    //    ##  ##  ####    ##    ##       ##   ##   ##  #### ######### ##
+    //    ##  ##   ###    ##    ##       ##    ##  ##   ### ##     ## ##
+    //   #### ##    ##    ##    ######## ##     ## ##    ## ##     ## ######## 
     //
-    //
+    //   ##     ## ######## ##       ########  ######## ########   ######
+    //   ##     ## ##       ##       ##     ## ##       ##     ## ##    ##
+    //   ##     ## ##       ##       ##     ## ##       ##     ## ##
+    //   ######### ######   ##       ########  ######   ########   ######
+    //   ##     ## ##       ##       ##        ##       ##   ##         ##
+    //   ##     ## ##       ##       ##        ##       ##    ##  ##    ##
+    //   ##     ## ######## ######## ##        ######## ##     ##  ######
     //
     // Internal helper methods:
     //
-    //
-    //
+
+    private String recordedValuesToString() {
+        String output = "";
+        try {
+            for (int i = 0; i < countsArrayLength; i++) {
+                if (getCountAtIndex(i) != 0) {
+                    output += String.format("[%d] : %d\n", i, getCountAtIndex(i));
+                }
+            }
+            return output;
+        } catch(Exception ex) {
+            output += "!!! Exception thown in value iteration...\n";
+        }
+        return output;
+    }
+
+    @Override
+    public String toString() {
+        String output = "AbstractHistogram:\n";
+        output += super.toString();
+        output += recordedValuesToString();
+        return output;
+    }
 
     void establishInternalTackingValues() {
         establishInternalTackingValues(countsArrayLength);
diff --git a/src/main/java/org/HdrHistogram/AbstractHistogramIterator.java b/src/main/java/org/HdrHistogram/AbstractHistogramIterator.java
index 771094d..59d4261 100644
--- a/src/main/java/org/HdrHistogram/AbstractHistogramIterator.java
+++ b/src/main/java/org/HdrHistogram/AbstractHistogramIterator.java
@@ -9,6 +9,7 @@ package org.HdrHistogram;
 
 import java.util.ConcurrentModificationException;
 import java.util.Iterator;
+import java.util.NoSuchElementException;
 
 /**
  * Used for iterating through histogram values.
@@ -103,7 +104,7 @@ abstract class AbstractHistogramIterator implements Iterator<HistogramIterationV
             (totalCountToCurrentIndex > arrayTotalCount)) {
             throw new ConcurrentModificationException();
         }
-        throw new ArrayIndexOutOfBoundsException();
+        throw new NoSuchElementException();
     }
 
     /**
diff --git a/src/main/java/org/HdrHistogram/AtomicHistogram.java b/src/main/java/org/HdrHistogram/AtomicHistogram.java
index cbf7d3c..146e985 100644
--- a/src/main/java/org/HdrHistogram/AtomicHistogram.java
+++ b/src/main/java/org/HdrHistogram/AtomicHistogram.java
@@ -10,7 +10,6 @@ package org.HdrHistogram;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.nio.ByteBuffer;
-import java.nio.LongBuffer;
 import java.util.concurrent.atomic.*;
 import java.util.zip.DataFormatException;
 
@@ -156,8 +155,8 @@ public class AtomicHistogram extends Histogram {
     }
 
     /**
-     * Construct a AtomicHistogram given the Highest value to be tracked and a number of significant decimal digits. The
-     * histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
+     * Construct a AtomicHistogram given the Highest value to be tracked and a number of significant decimal digits.
+     * The histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
      *
      * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
      *                              integer that is {@literal >=} 2.
@@ -185,7 +184,8 @@ public class AtomicHistogram extends Histogram {
      *                                       decimal digits to which the histogram will maintain value resolution
      *                                       and separation. Must be a non-negative integer between 0 and 5.
      */
-    public AtomicHistogram(final long lowestDiscernibleValue, final long highestTrackableValue, final int numberOfSignificantValueDigits) {
+    public AtomicHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
+                           final int numberOfSignificantValueDigits) {
         super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits, false);
         counts = new AtomicLongArray(countsArrayLength);
         wordSizeInBytes = 8;
@@ -210,8 +210,7 @@ public class AtomicHistogram extends Histogram {
      */
     public static AtomicHistogram decodeFromByteBuffer(final ByteBuffer buffer,
                                                        final long minBarForHighestTrackableValue) {
-        return (AtomicHistogram) decodeFromByteBuffer(buffer, AtomicHistogram.class,
-                minBarForHighestTrackableValue);
+        return decodeFromByteBuffer(buffer, AtomicHistogram.class, minBarForHighestTrackableValue);
     }
 
     /**
@@ -222,10 +221,26 @@ public class AtomicHistogram extends Histogram {
      * @throws DataFormatException on error parsing/decompressing the buffer
      */
     public static AtomicHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
-                                                                 final long minBarForHighestTrackableValue) throws DataFormatException {
+                                                                 final long minBarForHighestTrackableValue)
+            throws DataFormatException {
         return decodeFromCompressedByteBuffer(buffer, AtomicHistogram.class, minBarForHighestTrackableValue);
     }
 
+    /**
+     * Construct a new AtomicHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A AtomicHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static AtomicHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
+    }
+
     private void readObject(final ObjectInputStream o)
             throws IOException, ClassNotFoundException {
         o.defaultReadObject();
diff --git a/src/main/java/org/HdrHistogram/Base64Helper.java b/src/main/java/org/HdrHistogram/Base64Helper.java
index f588d82..875e5c5 100644
--- a/src/main/java/org/HdrHistogram/Base64Helper.java
+++ b/src/main/java/org/HdrHistogram/Base64Helper.java
@@ -26,7 +26,7 @@ import java.lang.reflect.Method;
  * Java SE version (e.g. beyond 7 or before 9).
  *
  */
-public class Base64Helper {
+class Base64Helper {
 
     /**
      * Converts an array of bytes into a Base64 string.
diff --git a/src/main/java/org/HdrHistogram/ConcurrentDoubleHistogram.java b/src/main/java/org/HdrHistogram/ConcurrentDoubleHistogram.java
index e188e43..15f4d3f 100644
--- a/src/main/java/org/HdrHistogram/ConcurrentDoubleHistogram.java
+++ b/src/main/java/org/HdrHistogram/ConcurrentDoubleHistogram.java
@@ -7,6 +7,9 @@
 
 package org.HdrHistogram;
 
+import java.nio.ByteBuffer;
+import java.util.zip.DataFormatException;
+
 /**
  * <h3>A floating point values High Dynamic Range (HDR) Histogram that supports safe concurrent recording
  * operations.</h3>
@@ -22,7 +25,7 @@ package org.HdrHistogram;
  * potentially concurrent, multi-threaded updates that would safely work in the presence of queries, copies, or
  * additions of histogram objects should either take care to externally synchronize and/or order their access,
  * use the {@link SynchronizedDoubleHistogram} variant, or (recommended) use the {@link DoubleRecorder}
- * class, which is intended for this purpose.
+ * or {@link SingleWriterDoubleRecorder} which are intended for this purpose.
  * <p>
  * {@link ConcurrentDoubleHistogram} supports the recording and analyzing sampled data value counts across a
  * configurable dynamic range of floating point (double) values, with configurable value precision within the range.
@@ -77,7 +80,7 @@ public class ConcurrentDoubleHistogram extends DoubleHistogram {
      *                                       separation. Must be a non-negative integer between 0 and 5.
      */
     public ConcurrentDoubleHistogram(final long highestToLowestValueRatio, final int numberOfSignificantValueDigits) {
-        super(highestToLowestValueRatio, numberOfSignificantValueDigits, ConcurrentHistogram.class);
+        this(highestToLowestValueRatio, numberOfSignificantValueDigits, ConcurrentHistogram.class);
     }
 
     /**
@@ -88,4 +91,65 @@ public class ConcurrentDoubleHistogram extends DoubleHistogram {
     public ConcurrentDoubleHistogram(final DoubleHistogram source) {
         super(source);
     }
+
+    ConcurrentDoubleHistogram(final long highestToLowestValueRatio,
+                              final int numberOfSignificantValueDigits,
+                              final Class<? extends AbstractHistogram> internalCountsHistogramClass) {
+        super(highestToLowestValueRatio, numberOfSignificantValueDigits, internalCountsHistogramClass);
+    }
+
+    ConcurrentDoubleHistogram(final long highestToLowestValueRatio,
+                    final int numberOfSignificantValueDigits,
+                    final Class<? extends AbstractHistogram> internalCountsHistogramClass,
+                    AbstractHistogram internalCountsHistogram) {
+        super(
+                highestToLowestValueRatio,
+                numberOfSignificantValueDigits,
+                internalCountsHistogramClass,
+                internalCountsHistogram
+        );
+    }
+
+    /**
+     * Construct a new ConcurrentDoubleHistogram by decoding it from a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestToLowestValueRatio Force highestTrackableValue to be set at least this high
+     * @return The newly constructed ConcurrentDoubleHistogram
+     */
+    public static ConcurrentDoubleHistogram decodeFromByteBuffer(
+            final ByteBuffer buffer,
+            final long minBarForHighestToLowestValueRatio) {
+        try {
+            int cookie = buffer.getInt();
+            if (!isNonCompressedDoubleHistogramCookie(cookie)) {
+                throw new IllegalArgumentException("The buffer does not contain a DoubleHistogram");
+            }
+            ConcurrentDoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                    ConcurrentDoubleHistogram.class, ConcurrentHistogram.class,
+                    minBarForHighestToLowestValueRatio);
+            return histogram;
+        } catch (DataFormatException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    /**
+     * Construct a new ConcurrentDoubleHistogram by decoding it from a compressed form in a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestToLowestValueRatio Force highestTrackableValue to be set at least this high
+     * @return The newly constructed ConcurrentDoubleHistogram
+     * @throws DataFormatException on error parsing/decompressing the buffer
+     */
+    public static ConcurrentDoubleHistogram decodeFromCompressedByteBuffer(
+            final ByteBuffer buffer,
+            final long minBarForHighestToLowestValueRatio) throws DataFormatException {
+        int cookie = buffer.getInt();
+        if (!isCompressedDoubleHistogramCookie(cookie)) {
+            throw new IllegalArgumentException("The buffer does not contain a compressed DoubleHistogram");
+        }
+        ConcurrentDoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                ConcurrentDoubleHistogram.class, ConcurrentHistogram.class,
+                minBarForHighestToLowestValueRatio);
+        return histogram;
+    }
 }
diff --git a/src/main/java/org/HdrHistogram/ConcurrentHistogram.java b/src/main/java/org/HdrHistogram/ConcurrentHistogram.java
index 77f0efa..bf29c95 100644
--- a/src/main/java/org/HdrHistogram/ConcurrentHistogram.java
+++ b/src/main/java/org/HdrHistogram/ConcurrentHistogram.java
@@ -10,7 +10,6 @@ package org.HdrHistogram;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.nio.ByteBuffer;
-import java.nio.LongBuffer;
 import java.util.concurrent.atomic.AtomicLongArray;
 import java.util.concurrent.atomic.AtomicLongFieldUpdater;
 import java.util.zip.DataFormatException;
@@ -38,14 +37,15 @@ import java.util.zip.DataFormatException;
  * See package description for {@link org.HdrHistogram} for details.
  */
 
+@SuppressWarnings("unused")
 public class ConcurrentHistogram extends Histogram {
 
     static final AtomicLongFieldUpdater<ConcurrentHistogram> totalCountUpdater =
             AtomicLongFieldUpdater.newUpdater(ConcurrentHistogram.class, "totalCount");
     volatile long totalCount;
 
-    volatile AtomicLongArrayWithNormalizingOffset activeCounts;
-    volatile AtomicLongArrayWithNormalizingOffset inactiveCounts;
+    volatile ConcurrentArrayWithNormalizingOffset activeCounts;
+    volatile ConcurrentArrayWithNormalizingOffset inactiveCounts;
     transient WriterReaderPhaser wrp = new WriterReaderPhaser();
 
     @Override
@@ -53,16 +53,16 @@ public class ConcurrentHistogram extends Histogram {
         try {
             wrp.readerLock();
 
-            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / integerToDoubleValueConversionRatio;
+            inactiveCounts.setDoubleToIntegerValueConversionRatio(1.0 / integerToDoubleValueConversionRatio);
 
             // switch active and inactive:
-            AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
+            ConcurrentArrayWithNormalizingOffset tmp = activeCounts;
             activeCounts = inactiveCounts;
             inactiveCounts = tmp;
 
             wrp.flipPhase();
 
-            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / integerToDoubleValueConversionRatio;
+            inactiveCounts.setDoubleToIntegerValueConversionRatio(1.0 / integerToDoubleValueConversionRatio);
 
             // switch active and inactive again:
             tmp = activeCounts;
@@ -114,7 +114,7 @@ public class ConcurrentHistogram extends Histogram {
     void incrementCountAtIndex(final int index) {
         long criticalValue = wrp.writerCriticalSectionEnter();
         try {
-            activeCounts.incrementAndGet(
+            activeCounts.atomicIncrement(
                     normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
         } finally {
             wrp.writerCriticalSectionExit(criticalValue);
@@ -125,7 +125,7 @@ public class ConcurrentHistogram extends Histogram {
     void addToCountAtIndex(final int index, final long value) {
         long criticalValue = wrp.writerCriticalSectionEnter();
         try {
-            activeCounts.addAndGet(
+            activeCounts.atomicAdd(
                     normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), value);
         } finally {
             wrp.writerCriticalSectionExit(criticalValue);
@@ -141,7 +141,8 @@ public class ConcurrentHistogram extends Histogram {
             activeCounts.lazySet(
                     normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), value);
             inactiveCounts.lazySet(
-                    normalizeIndex(index, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length()), 0);
+                    normalizeIndex(index, inactiveCounts.getNormalizingIndexOffset(),
+                            inactiveCounts.length()), 0);
         } finally {
             wrp.readerUnlock();
         }
@@ -164,9 +165,9 @@ public class ConcurrentHistogram extends Histogram {
     void recordConvertedDoubleValue(final double value) {
         long criticalValue = wrp.writerCriticalSectionEnter();
         try {
-            long integerValue = (long) (value * activeCounts.doubleToIntegerValueConversionRatio);
+            long integerValue = (long) (value * activeCounts.getDoubleToIntegerValueConversionRatio());
             int index = countsArrayIndex(integerValue);
-            activeCounts.incrementAndGet(
+            activeCounts.atomicIncrement(
                     normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
             updateMinAndMax(integerValue);
             incrementTotalCount();
@@ -176,12 +177,13 @@ public class ConcurrentHistogram extends Histogram {
     }
 
     @Override
-    public void recordConvertedDoubleValueWithCount(final double value, final long count) throws ArrayIndexOutOfBoundsException {
+    public void recordConvertedDoubleValueWithCount(final double value, final long count)
+            throws ArrayIndexOutOfBoundsException {
         long criticalValue = wrp.writerCriticalSectionEnter();
         try {
-            long integerValue = (long) (value * activeCounts.doubleToIntegerValueConversionRatio);
+            long integerValue = (long) (value * activeCounts.getDoubleToIntegerValueConversionRatio());
             int index = countsArrayIndex(integerValue);
-            activeCounts.addAndGet(
+            activeCounts.atomicAdd(
                     normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), count);
             updateMinAndMax(integerValue);
             addToTotalCount(count);
@@ -197,11 +199,12 @@ public class ConcurrentHistogram extends Histogram {
 
     @Override
     void setNormalizingIndexOffset(final int normalizingIndexOffset) {
-        setNormalizingIndexOffset(normalizingIndexOffset, 0, false, getIntegerToDoubleValueConversionRatio());
+        setNormalizingIndexOffset(normalizingIndexOffset, 0,
+                false, getIntegerToDoubleValueConversionRatio());
     }
 
     private void setNormalizingIndexOffset(
-            final int normalizingIndexOffset,
+            final int newNormalizingIndexOffset,
             final int shiftedAmount,
             final boolean lowestHalfBucketPopulated,
             final double newIntegerToDoubleValueConversionRatio) {
@@ -213,54 +216,22 @@ public class ConcurrentHistogram extends Histogram {
 
             assert (activeCounts.getNormalizingIndexOffset() == inactiveCounts.getNormalizingIndexOffset());
 
-            if (normalizingIndexOffset == activeCounts.getNormalizingIndexOffset()) {
+            if (newNormalizingIndexOffset == activeCounts.getNormalizingIndexOffset()) {
                 return; // Nothing to do.
             }
 
-            // Save and clear the inactive 0 value count:
-            int zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
-            long inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
-            inactiveCounts.lazySet(zeroIndex, 0);
-
-            // Change the normalizingIndexOffset on the current inactiveCounts:
-            inactiveCounts.setNormalizingIndexOffset(normalizingIndexOffset);
-
-            // Handle the inactive lowest half bucket:
-            if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
-                shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount, zeroIndex);
-            }
-
-            // Restore the inactive 0 value count:
-            zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
-            inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);
-
-            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / newIntegerToDoubleValueConversionRatio;
+            setNormalizingIndexOffsetForInactive(newNormalizingIndexOffset, shiftedAmount,
+                    lowestHalfBucketPopulated, newIntegerToDoubleValueConversionRatio);
 
             // switch active and inactive:
-            AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
+            ConcurrentArrayWithNormalizingOffset tmp = activeCounts;
             activeCounts = inactiveCounts;
             inactiveCounts = tmp;
 
             wrp.flipPhase();
 
-            // Save and clear the newly inactive 0 value count:
-            zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
-            inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
-            inactiveCounts.lazySet(zeroIndex, 0);
-
-            // Change the normalizingIndexOffset on the newly inactiveCounts:
-            inactiveCounts.setNormalizingIndexOffset(normalizingIndexOffset);
-
-            // Handle the newly inactive lowest half bucket:
-            if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
-                shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount, zeroIndex);
-            }
-
-            // Restore the newly inactive 0 value count:
-            zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
-            inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);
-
-            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / newIntegerToDoubleValueConversionRatio;
+            setNormalizingIndexOffsetForInactive(newNormalizingIndexOffset, shiftedAmount,
+                    lowestHalfBucketPopulated, newIntegerToDoubleValueConversionRatio);
 
             // switch active and inactive again:
             tmp = activeCounts;
@@ -277,6 +248,34 @@ public class ConcurrentHistogram extends Histogram {
         }
     }
 
+    private void setNormalizingIndexOffsetForInactive(final int newNormalizingIndexOffset,
+                                                      final int shiftedAmount,
+                                                      final boolean lowestHalfBucketPopulated,
+                                                      final double newIntegerToDoubleValueConversionRatio) {
+        int zeroIndex;
+        long inactiveZeroValueCount;
+
+        // Save and clear the inactive 0 value count:
+        zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(),
+                inactiveCounts.length());
+        inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
+        inactiveCounts.lazySet(zeroIndex, 0);
+
+        // Change the normalizingIndexOffset on the current inactiveCounts:
+        inactiveCounts.setNormalizingIndexOffset(newNormalizingIndexOffset);
+
+        // Handle the inactive lowest half bucket:
+        if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
+            shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount, zeroIndex);
+        }
+
+        // Restore the inactive 0 value count:
+        zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
+        inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);
+
+        inactiveCounts.setDoubleToIntegerValueConversionRatio(1.0 / newIntegerToDoubleValueConversionRatio);
+    }
+
     private void shiftLowestInactiveHalfBucketContentsLeft(final int shiftAmount, final int preShiftZeroIndex) {
         final int numberOfBinaryOrdersOfMagnitude = shiftAmount >> subBucketHalfCountMagnitude;
 
@@ -330,6 +329,10 @@ public class ConcurrentHistogram extends Histogram {
         }
     }
 
+    ConcurrentArrayWithNormalizingOffset allocateArray(int length, int normalizingIndexOffset) {
+        return new AtomicLongArrayWithNormalizingOffset(length, normalizingIndexOffset);
+    }
+
     @Override
     void resize(final long newHighestTrackableValue) {
         try {
@@ -347,27 +350,21 @@ public class ConcurrentHistogram extends Histogram {
             }
 
             // Allocate both counts arrays here, so if one allocation fails, neither will "take":
-            AtomicLongArrayWithNormalizingOffset newInactiveCounts1 =
-                    new AtomicLongArrayWithNormalizingOffset(
-                            newArrayLength,
-                            inactiveCounts.getNormalizingIndexOffset()
-                    );
-            AtomicLongArrayWithNormalizingOffset newInactiveCounts2 =
-                    new AtomicLongArrayWithNormalizingOffset(
-                            newArrayLength,
-                            activeCounts.getNormalizingIndexOffset()
-                    );
+            ConcurrentArrayWithNormalizingOffset newInactiveCounts1 =
+                    allocateArray(newArrayLength, inactiveCounts.getNormalizingIndexOffset());
+            ConcurrentArrayWithNormalizingOffset newInactiveCounts2 =
+                    allocateArray(newArrayLength, activeCounts.getNormalizingIndexOffset());
 
 
             // Resize the current inactiveCounts:
-            AtomicLongArrayWithNormalizingOffset oldInactiveCounts = inactiveCounts;
+            ConcurrentArrayWithNormalizingOffset oldInactiveCounts = inactiveCounts;
             inactiveCounts = newInactiveCounts1;
 
             // Copy inactive contents to newly sized inactiveCounts:
             copyInactiveCountsContentsOnResize(oldInactiveCounts, countsDelta);
 
             // switch active and inactive:
-            AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
+            ConcurrentArrayWithNormalizingOffset tmp = activeCounts;
             activeCounts = inactiveCounts;
             inactiveCounts = tmp;
 
@@ -401,34 +398,34 @@ public class ConcurrentHistogram extends Histogram {
         }
     }
 
-    private void copyInactiveCountsContentsOnResize(
-            AtomicLongArrayWithNormalizingOffset oldInactiveCounts, int countsDelta) {
+    void copyInactiveCountsContentsOnResize(
+            ConcurrentArrayWithNormalizingOffset oldInactiveCounts, int countsDelta) {
         int oldNormalizedZeroIndex =
                 normalizeIndex(0,
                         oldInactiveCounts.getNormalizingIndexOffset(),
                         oldInactiveCounts.length());
 
-        // Copy old inactive contents to (current) newly sized inactiveCounts:
-        for (int i = 0; i < oldInactiveCounts.length(); i++) {
-            inactiveCounts.lazySet(i, oldInactiveCounts.get(i));
-        }
-        if (oldNormalizedZeroIndex != 0) {
+        if (oldNormalizedZeroIndex == 0) {
+            // Copy old inactive contents to (current) newly sized inactiveCounts, in place:
+            for (int i = 0; i < oldInactiveCounts.length(); i++) {
+                inactiveCounts.lazySet(i, oldInactiveCounts.get(i));
+            }
+        } else {
             // We need to shift the stuff from the zero index and up to the end of the array:
-            int newNormalizedZeroIndex = oldNormalizedZeroIndex + countsDelta;
-            int lengthToCopy = (inactiveCounts.length() - countsDelta) - oldNormalizedZeroIndex;
-            int src, dst;
-            for (src = oldNormalizedZeroIndex, dst = newNormalizedZeroIndex;
-                 src < oldNormalizedZeroIndex + lengthToCopy;
-                 src++, dst++) {
-                inactiveCounts.lazySet(dst, oldInactiveCounts.get(src));
+
+            // Copy everything up to the oldNormalizedZeroIndex in place:
+            for (int fromIndex = 0; fromIndex < oldNormalizedZeroIndex; fromIndex++) {
+                inactiveCounts.lazySet(fromIndex, oldInactiveCounts.get(fromIndex));
             }
-            for (dst = oldNormalizedZeroIndex; dst < newNormalizedZeroIndex; dst++) {
-                inactiveCounts.lazySet(dst, 0);
+
+            // Copy everything from the oldNormalizedZeroIndex to the end with an index delta shift:
+            for (int fromIndex = oldNormalizedZeroIndex; fromIndex < oldInactiveCounts.length(); fromIndex++) {
+                int toIndex = fromIndex + countsDelta;
+                inactiveCounts.lazySet(toIndex, oldInactiveCounts.get(fromIndex));
             }
         }
     }
 
-
     @Override
     public void setAutoResize(final boolean autoResize) {
         this.autoResize = true;
@@ -503,8 +500,8 @@ public class ConcurrentHistogram extends Histogram {
     }
 
     /**
-     * Construct a ConcurrentHistogram given the Highest value to be tracked and a number of significant decimal digits. The
-     * histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
+     * Construct a ConcurrentHistogram given the Highest value to be tracked and a number of significant decimal
+     * digits. The histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
      *
      * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
      *                              integer that is {@literal >=} 2.
@@ -534,10 +531,8 @@ public class ConcurrentHistogram extends Histogram {
      */
     public ConcurrentHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
                                final int numberOfSignificantValueDigits) {
-        super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits, false);
-        activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
-        inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
-        wordSizeInBytes = 8;
+        this(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits,
+                true);
     }
 
     /**
@@ -546,9 +541,26 @@ public class ConcurrentHistogram extends Histogram {
      * @param source The source histogram to duplicate
      */
     public ConcurrentHistogram(final AbstractHistogram source) {
-        super(source, false);
-        activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
-        inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
+        this(source, true);
+    }
+
+    ConcurrentHistogram(final AbstractHistogram source, boolean allocateCountsArray) {
+        super(source,false);
+        if (allocateCountsArray) {
+            activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
+            inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
+        }
+        wordSizeInBytes = 8;
+    }
+
+    ConcurrentHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
+                        final int numberOfSignificantValueDigits, boolean allocateCountsArray) {
+        super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits,
+                false);
+        if (allocateCountsArray) {
+            activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
+            inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
+        }
         wordSizeInBytes = 8;
     }
 
@@ -560,8 +572,7 @@ public class ConcurrentHistogram extends Histogram {
      */
     public static ConcurrentHistogram decodeFromByteBuffer(final ByteBuffer buffer,
                                                            final long minBarForHighestTrackableValue) {
-        return (ConcurrentHistogram) decodeFromByteBuffer(buffer, ConcurrentHistogram.class,
-                minBarForHighestTrackableValue);
+        return decodeFromByteBuffer(buffer, ConcurrentHistogram.class, minBarForHighestTrackableValue);
     }
 
     /**
@@ -572,9 +583,24 @@ public class ConcurrentHistogram extends Histogram {
      * @throws java.util.zip.DataFormatException on error parsing/decompressing the buffer
      */
     public static ConcurrentHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
-                                                                     final long minBarForHighestTrackableValue) throws DataFormatException {
-        return (ConcurrentHistogram) decodeFromCompressedByteBuffer(buffer, ConcurrentHistogram.class,
-                minBarForHighestTrackableValue);
+                                                                     final long minBarForHighestTrackableValue)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(buffer, ConcurrentHistogram.class, minBarForHighestTrackableValue);
+    }
+
+    /**
+     * Construct a new ConcurrentHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A ConcurrentHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static ConcurrentHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
     }
 
     private void readObject(final ObjectInputStream o)
@@ -583,15 +609,6 @@ public class ConcurrentHistogram extends Histogram {
         wrp = new WriterReaderPhaser();
     }
 
-    @Override
-    synchronized void fillCountsArrayFromBuffer(final ByteBuffer buffer, final int length) {
-        LongBuffer logbuffer = buffer.asLongBuffer();
-        for (int i = 0; i < length; i++) {
-            inactiveCounts.lazySet(i, logbuffer.get());
-            activeCounts.lazySet(i, 0);
-        }
-    }
-
     @Override
     synchronized void fillBufferFromCountsArray(final ByteBuffer buffer) {
         try {
@@ -602,8 +619,31 @@ public class ConcurrentHistogram extends Histogram {
         }
     }
 
-    static class AtomicLongArrayWithNormalizingOffset extends AtomicLongArray {
+    interface ConcurrentArrayWithNormalizingOffset {
+
+        int getNormalizingIndexOffset();
+
+        void setNormalizingIndexOffset(int normalizingIndexOffset);
+
+        double getDoubleToIntegerValueConversionRatio();
+
+        void setDoubleToIntegerValueConversionRatio(double doubleToIntegerValueConversionRatio);
+
+        int getEstimatedFootprintInBytes();
+
+        long get(int index);
 
+        void atomicIncrement(int index);
+
+        void atomicAdd(int index, long valueToAdd);
+
+        void lazySet(int index, long newValue);
+
+        int length();
+    }
+
+    static class AtomicLongArrayWithNormalizingOffset extends AtomicLongArray
+            implements ConcurrentArrayWithNormalizingOffset {
         private int normalizingIndexOffset;
         private double doubleToIntegerValueConversionRatio;
 
@@ -612,12 +652,40 @@ public class ConcurrentHistogram extends Histogram {
             this.normalizingIndexOffset = normalizingIndexOffset;
         }
 
+        @Override
         public int getNormalizingIndexOffset() {
             return normalizingIndexOffset;
         }
 
+        @Override
         public void setNormalizingIndexOffset(int normalizingIndexOffset) {
             this.normalizingIndexOffset = normalizingIndexOffset;
         }
+
+        @Override
+        public double getDoubleToIntegerValueConversionRatio() {
+            return doubleToIntegerValueConversionRatio;
+        }
+
+        @Override
+        public void setDoubleToIntegerValueConversionRatio(double doubleToIntegerValueConversionRatio) {
+            this.doubleToIntegerValueConversionRatio = doubleToIntegerValueConversionRatio;
+        }
+
+        @Override
+        public int getEstimatedFootprintInBytes() {
+            return 256 + (8 * this.length());
+        }
+
+        @Override
+        public void atomicIncrement(int index) {
+            incrementAndGet(index);
+        }
+
+        @Override
+        public void atomicAdd(int index, long valueToAdd) {
+            addAndGet(index, valueToAdd);
+        }
+
     }
 }
diff --git a/src/main/java/org/HdrHistogram/DoubleHistogram.java b/src/main/java/org/HdrHistogram/DoubleHistogram.java
index ba42b7a..28502bb 100644
--- a/src/main/java/org/HdrHistogram/DoubleHistogram.java
+++ b/src/main/java/org/HdrHistogram/DoubleHistogram.java
@@ -20,8 +20,8 @@ import java.util.zip.Deflater;
  * <p>
  * It is important to note that {@link DoubleHistogram} is not thread-safe, and does not support safe concurrent
  * recording by multiple threads. If concurrent operation is required, consider usings
- * {@link ConcurrentDoubleHistogram}, {@link SynchronizedDoubleHistogram}, or(recommended)
- * {@link DoubleRecorder}, which are intended for this purpose.
+ * {@link ConcurrentDoubleHistogram}, {@link SynchronizedDoubleHistogram},
+ * or (recommended) {@link DoubleRecorder} or {@link SingleWriterDoubleRecorder} which are intended for this purpose.
  * <p>
  * {@link DoubleHistogram} supports the recording and analyzing sampled data value counts across a
  * configurable dynamic range of floating point (double) values, with configurable value precision within the range.
@@ -134,7 +134,7 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
         this(highestToLowestValueRatio, numberOfSignificantValueDigits, internalCountsHistogramClass, null);
     }
 
-    private DoubleHistogram(final long highestToLowestValueRatio,
+    DoubleHistogram(final long highestToLowestValueRatio,
                             final int numberOfSignificantValueDigits,
                             final Class<? extends AbstractHistogram> internalCountsHistogramClass,
                             AbstractHistogram internalCountsHistogram) {
@@ -215,13 +215,8 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
             // Set our double tracking range and internal histogram:
             init(highestToLowestValueRatio, initialLowestValueInAutoRange, valuesHistogram);
 
-        } catch (NoSuchMethodException ex) {
-            throw new IllegalArgumentException(ex);
-        } catch (IllegalAccessException ex) {
-            throw new IllegalArgumentException(ex);
-        } catch (InstantiationException ex) {
-            throw new IllegalArgumentException(ex);
-        } catch (InvocationTargetException ex) {
+        } catch (NoSuchMethodException | IllegalAccessException |
+                InstantiationException | InvocationTargetException ex) {
             throw new IllegalArgumentException(ex);
         }
     }
@@ -334,23 +329,57 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
     }
 
     private void recordCountAtValue(final long count, final double value) throws ArrayIndexOutOfBoundsException {
-        if ((value < currentLowestValueInAutoRange) || (value >= currentHighestValueLimitInAutoRange)) {
-            // Zero is valid and needs no auto-ranging, but also rare enough that we should deal
-            // with it on the slow path...
-            autoAdjustRangeForValue(value);
+        int throwCount = 0;
+        while (true) {
+            if ((value < currentLowestValueInAutoRange) || (value >= currentHighestValueLimitInAutoRange)) {
+                // Zero is valid and needs no auto-ranging, but also rare enough that we should deal
+                // with it on the slow path...
+                autoAdjustRangeForValue(value);
+            }
+            try {
+                integerValuesHistogram.recordConvertedDoubleValueWithCount(value, count);
+                return;
+            } catch (ArrayIndexOutOfBoundsException ex) {
+                // A race that would pass the auto-range check above and would still take an AIOOB
+                // can only occur due to a value that would have been valid becoming invalid due
+                // to a concurrent adjustment operation. Such adjustment operations can happen no
+                // more than 64 times in the entire lifetime of the Histogram, which makes it safe
+                // to retry with no fear of live-locking.
+                if (++throwCount > 64) {
+                    // For the retry check to not detect an out of range attempt after 64 retries
+                    // should be  theoretically impossible, and would indicate a bug.
+                    throw new ArrayIndexOutOfBoundsException(
+                            "BUG: Unexpected non-transient AIOOB Exception caused by:\n" + ex);
+                }
+            }
         }
-
-        integerValuesHistogram.recordConvertedDoubleValueWithCount(value, count);
     }
 
     private void recordSingleValue(final double value) throws ArrayIndexOutOfBoundsException {
-        if ((value < currentLowestValueInAutoRange) || (value >= currentHighestValueLimitInAutoRange)) {
-            // Zero is valid and needs no auto-ranging, but also rare enough that we should deal
-            // with it on the slow path...
-            autoAdjustRangeForValue(value);
+        int throwCount = 0;
+        while (true) {
+            if ((value < currentLowestValueInAutoRange) || (value >= currentHighestValueLimitInAutoRange)) {
+                // Zero is valid and needs no auto-ranging, but also rare enough that we should deal
+                // with it on the slow path...
+                autoAdjustRangeForValue(value);
+            }
+            try {
+                integerValuesHistogram.recordConvertedDoubleValue(value);
+                return;
+            } catch (ArrayIndexOutOfBoundsException ex) {
+                // A race that would pass the auto-range check above and would still take an AIOOB
+                // can only occur due to a value that would have been valid becoming invalid due
+                // to a concurrent adjustment operation. Such adjustment operations can happen no
+                // more than 64 times in the entire lifetime of the Histogram, which makes it safe
+                // to retry with no fear of live-locking.
+                if (++throwCount > 64) {
+                    // For the retry check to not detect an out of range attempt after 64 retries
+                    // should be  theoretically impossible, and would indicate a bug.
+                    throw new ArrayIndexOutOfBoundsException(
+                            "BUG: Unexpected non-transient AIOOB Exception caused by:\n" + ex);
+                }
+            }
         }
-
-        integerValuesHistogram.recordConvertedDoubleValue(value);
     }
 
     private void recordValueWithCountAndExpectedInterval(final double value, final long count,
@@ -1432,11 +1461,11 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
         return isCompressedDoubleHistogramCookie(cookie) || isNonCompressedDoubleHistogramCookie(cookie);
     }
 
-    private static boolean isCompressedDoubleHistogramCookie(int cookie) {
+    static boolean isCompressedDoubleHistogramCookie(int cookie) {
         return (cookie == DHIST_compressedEncodingCookie);
     }
 
-    private static boolean isNonCompressedDoubleHistogramCookie(int cookie) {
+    static boolean isNonCompressedDoubleHistogramCookie(int cookie) {
         return (cookie == DHIST_encodingCookie);
     }
 
@@ -1484,9 +1513,12 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
         return encodeIntoCompressedByteBuffer(targetBuffer, Deflater.DEFAULT_COMPRESSION);
     }
 
-    private static DoubleHistogram constructHistogramFromBuffer(
+    private static final Class[] constructorArgTypes = {long.class, int.class, Class.class, AbstractHistogram.class};
+
+    static <T extends DoubleHistogram> T constructHistogramFromBuffer(
             int cookie,
             final ByteBuffer buffer,
+            final Class<T> doubleHistogramClass,
             final Class<? extends AbstractHistogram> histogramClass,
             final long minBarForHighestToLowestValueRatio) throws DataFormatException {
         int numberOfSignificantValueDigits = buffer.getInt();
@@ -1499,16 +1531,26 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
             valuesHistogram =
                     AbstractHistogram.decodeFromCompressedByteBuffer(buffer, histogramClass, minBarForHighestToLowestValueRatio);
         } else {
-            throw new IllegalStateException("The buffer does not contain a DoubleHistogram");
+            throw new IllegalArgumentException("The buffer does not contain a DoubleHistogram");
+        }
+
+        try {
+            Constructor<T> doubleHistogramConstructor =
+                    doubleHistogramClass.getDeclaredConstructor(constructorArgTypes);
+
+            T histogram =
+                    doubleHistogramConstructor.newInstance(
+                            configuredHighestToLowestValueRatio,
+                            numberOfSignificantValueDigits,
+                            histogramClass,
+                            valuesHistogram
+                    );
+            histogram.setAutoResize(true);
+            return histogram;
+        } catch (NoSuchMethodException | InstantiationException |
+                IllegalAccessException | InvocationTargetException ex) {
+            throw new IllegalStateException("Unable to construct DoubleHistogram of type " + doubleHistogramClass);
         }
-        DoubleHistogram histogram =
-                new DoubleHistogram(
-                        configuredHighestToLowestValueRatio,
-                        numberOfSignificantValueDigits,
-                        histogramClass,
-                        valuesHistogram
-                );
-        return histogram;
     }
 
     /**
@@ -1543,7 +1585,8 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
             if (!isNonCompressedDoubleHistogramCookie(cookie)) {
                 throw new IllegalArgumentException("The buffer does not contain a DoubleHistogram");
             }
-            DoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer, internalCountsHistogramClass,
+            DoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                    DoubleHistogram.class, internalCountsHistogramClass,
                     minBarForHighestToLowestValueRatio);
             return histogram;
         } catch (DataFormatException ex) {
@@ -1584,11 +1627,27 @@ public class DoubleHistogram extends EncodableHistogram implements DoubleValueRe
         if (!isCompressedDoubleHistogramCookie(cookie)) {
             throw new IllegalArgumentException("The buffer does not contain a compressed DoubleHistogram");
         }
-        DoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer, internalCountsHistogramClass,
+        DoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                DoubleHistogram.class, internalCountsHistogramClass,
                 minBarForHighestToLowestValueRatio);
         return histogram;
     }
 
+    /**
+     * Construct a new DoubleHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A DoubleHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static DoubleHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
+    }
+
     //
     //
     //
diff --git a/src/main/java/org/HdrHistogram/DoubleRecorder.java b/src/main/java/org/HdrHistogram/DoubleRecorder.java
index 63b62ab..9bba0ef 100644
--- a/src/main/java/org/HdrHistogram/DoubleRecorder.java
+++ b/src/main/java/org/HdrHistogram/DoubleRecorder.java
@@ -44,23 +44,45 @@ public class DoubleRecorder implements DoubleValueRecorder {
 
     private final WriterReaderPhaser recordingPhaser = new WriterReaderPhaser();
 
-    private volatile InternalConcurrentDoubleHistogram activeHistogram;
-    private InternalConcurrentDoubleHistogram inactiveHistogram;
+    private volatile ConcurrentDoubleHistogram activeHistogram;
+    private ConcurrentDoubleHistogram inactiveHistogram;
 
     /**
      * Construct an auto-resizing {@link DoubleRecorder} using a precision stated as a number
      * of significant decimal digits.
+     * <p>
+     * Depending on the valuer of the <b><code>packed</code></b> parameter {@link DoubleRecorder} can be configuired to
+     * track value counts in a packed internal representation optimized for typical histogram recoded values are
+     * sparse in the value range and tend to be incremented in small unit counts. This packed representation tends
+     * to require significantly smaller amounts of stoarge when compared to unpacked representations, but can incur
+     * additional recording cost due to resizing and repacking operations that may
+     * occur as previously unrecorded values are encountered.
      *
      * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
      *                                       decimal digits to which the histogram will maintain value resolution
      *                                       and separation. Must be a non-negative integer between 0 and 5.
+     * @param packed Specifies whether the recorder will uses a packed internal representation or not.
      */
-    public DoubleRecorder(final int numberOfSignificantValueDigits) {
-        activeHistogram = new InternalConcurrentDoubleHistogram(instanceId, numberOfSignificantValueDigits);
+    public DoubleRecorder(final int numberOfSignificantValueDigits, boolean packed) {
+        activeHistogram = packed ?
+                new PackedInternalConcurrentDoubleHistogram(instanceId, numberOfSignificantValueDigits) :
+                new InternalConcurrentDoubleHistogram(instanceId, numberOfSignificantValueDigits);
         inactiveHistogram = null;
         activeHistogram.setStartTimeStamp(System.currentTimeMillis());
     }
 
+    /**
+     * Construct an auto-resizing {@link DoubleRecorder} using a precision stated as a number
+     * of significant decimal digits.
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public DoubleRecorder(final int numberOfSignificantValueDigits) {
+        this(numberOfSignificantValueDigits, false);
+    }
+
     /**
      * Construct a {@link DoubleRecorder} dynamic range of values to cover and a number of significant
      * decimal digits.
@@ -141,7 +163,7 @@ public class DoubleRecorder implements DoubleValueRecorder {
      * Get a new instance of an interval histogram, which will include a stable, consistent view of all value
      * counts accumulated since the last interval histogram was taken.
      * <p>
-     * Calling {@link DoubleRecorder#getIntervalHistogram()} will reset
+     * Calling {@code getIntervalHistogram()} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @return a histogram containing the value counts accumulated since the last interval histogram was taken.
@@ -154,24 +176,20 @@ public class DoubleRecorder implements DoubleValueRecorder {
      * Get an interval histogram, which will include a stable, consistent view of all value counts
      * accumulated since the last interval histogram was taken.
      * <p>
-     * {@link DoubleRecorder#getIntervalHistogram(DoubleHistogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)}
+     * {@code getIntervalHistogram(histogramToRecycle)}
      * accepts a previously returned interval histogram that can be recycled internally to avoid allocation
      * and content copying operations, and is therefore significantly more efficient for repeated use than
      * {@link DoubleRecorder#getIntervalHistogram()} and
      * {@link DoubleRecorder#getIntervalHistogramInto getIntervalHistogramInto()}. The provided
      * {@code histogramToRecycle} must
      * be either be null or an interval histogram returned by a previous call to
-     * {@link DoubleRecorder#getIntervalHistogram(DoubleHistogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} or
-     * {@link DoubleRecorder#getIntervalHistogram()}.
+     * {@code getIntervalHistogram(histogramToRecycle)} or {@link DoubleRecorder#getIntervalHistogram()}.
      * <p>
      * NOTE: The caller is responsible for not recycling the same returned interval histogram more than once. If
      * the same interval histogram instance is recycled more than once, behavior is undefined.
      * <p>
-     * Calling {@link DoubleRecorder#getIntervalHistogram(DoubleHistogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start accumulating value
-     * counts for the next interval
+     * Calling {@code getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start
+     * accumulating value counts for the next interval
      *
      * @param histogramToRecycle a previously returned interval histogram (from this instance of
      *                           {@link DoubleRecorder}) that may be recycled to avoid allocation and
@@ -216,7 +234,7 @@ public class DoubleRecorder implements DoubleValueRecorder {
                                                              boolean enforeContainingInstance) {
         // Verify that replacement histogram can validly be used as an inactive histogram replacement:
         validateFitAsReplacementHistogram(histogramToRecycle, enforeContainingInstance);
-        inactiveHistogram = (InternalConcurrentDoubleHistogram) histogramToRecycle;
+        inactiveHistogram = (ConcurrentDoubleHistogram) histogramToRecycle;
         performIntervalSample();
         DoubleHistogram sampledHistogram = inactiveHistogram;
         inactiveHistogram = null; // Once we expose the sample, we can't reuse it internally until it is recycled
@@ -227,7 +245,7 @@ public class DoubleRecorder implements DoubleValueRecorder {
      * Place a copy of the value counts accumulated since accumulated (since the last interval histogram
      * was taken) into {@code targetHistogram}.
      *
-     * Calling {@link DoubleRecorder#getIntervalHistogramInto}() will reset
+     * Calling {@code getIntervalHistogramInto(targetHistogram)} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @param targetHistogram the histogram into which the interval histogram's data should be copied
@@ -253,13 +271,21 @@ public class DoubleRecorder implements DoubleValueRecorder {
 
             // Make sure we have an inactive version to flip in:
             if (inactiveHistogram == null) {
-                inactiveHistogram = new InternalConcurrentDoubleHistogram(activeHistogram);
+                if (activeHistogram instanceof InternalConcurrentDoubleHistogram) {
+                    inactiveHistogram = new InternalConcurrentDoubleHistogram(
+                            (InternalConcurrentDoubleHistogram) activeHistogram);
+                } else if (activeHistogram instanceof PackedInternalConcurrentDoubleHistogram) {
+                    inactiveHistogram = new PackedInternalConcurrentDoubleHistogram(
+                            instanceId, activeHistogram.getNumberOfSignificantValueDigits());
+                } else {
+                    throw new IllegalStateException("Unexpected internal histogram type for activeHistogram");
+                }
             }
 
             inactiveHistogram.reset();
 
             // Swap active and inactive histograms:
-            final InternalConcurrentDoubleHistogram tempHistogram = inactiveHistogram;
+            final ConcurrentDoubleHistogram tempHistogram = inactiveHistogram;
             inactiveHistogram = activeHistogram;
             activeHistogram = tempHistogram;
 
@@ -277,7 +303,7 @@ public class DoubleRecorder implements DoubleValueRecorder {
         }
     }
 
-    private class InternalConcurrentDoubleHistogram extends ConcurrentDoubleHistogram {
+    private static class InternalConcurrentDoubleHistogram extends ConcurrentDoubleHistogram {
         private final long containingInstanceId;
 
         private InternalConcurrentDoubleHistogram(long id, int numberOfSignificantValueDigits) {
@@ -298,6 +324,15 @@ public class DoubleRecorder implements DoubleValueRecorder {
         }
     }
 
+    private static class PackedInternalConcurrentDoubleHistogram extends PackedConcurrentDoubleHistogram {
+        private final long containingInstanceId;
+
+        private PackedInternalConcurrentDoubleHistogram(long id, int numberOfSignificantValueDigits) {
+            super(numberOfSignificantValueDigits);
+            this.containingInstanceId = id;
+        }
+    }
+
     private void validateFitAsReplacementHistogram(DoubleHistogram replacementHistogram,
                                                    boolean enforeContainingInstance) {
         boolean bad = true;
@@ -307,7 +342,14 @@ public class DoubleRecorder implements DoubleValueRecorder {
                 &&
                 ((!enforeContainingInstance) ||
                         (((InternalConcurrentDoubleHistogram) replacementHistogram).containingInstanceId ==
-                                activeHistogram.containingInstanceId)
+                                ((InternalConcurrentDoubleHistogram) activeHistogram).containingInstanceId)
+                )) {
+            bad = false;
+        } else if ((replacementHistogram instanceof PackedInternalConcurrentDoubleHistogram)
+                &&
+                ((!enforeContainingInstance) ||
+                        (((PackedInternalConcurrentDoubleHistogram) replacementHistogram).containingInstanceId ==
+                                ((PackedInternalConcurrentDoubleHistogram) activeHistogram).containingInstanceId)
                 )) {
             bad = false;
         }
diff --git a/src/main/java/org/HdrHistogram/EncodableHistogram.java b/src/main/java/org/HdrHistogram/EncodableHistogram.java
index 5a42478..cc63631 100644
--- a/src/main/java/org/HdrHistogram/EncodableHistogram.java
+++ b/src/main/java/org/HdrHistogram/EncodableHistogram.java
@@ -37,7 +37,7 @@ public abstract class EncodableHistogram {
     public abstract double getMaxValueAsDouble();
 
     /**
-     * Decode a {@EncodableHistogram} from a compressed byte buffer. Will return either a
+     * Decode a {@link EncodableHistogram} from a compressed byte buffer. Will return either a
      * {@link org.HdrHistogram.Histogram} or {@link org.HdrHistogram.DoubleHistogram} depending
      * on the format found in the supplied buffer.
      *
diff --git a/src/main/java/org/HdrHistogram/Histogram.java b/src/main/java/org/HdrHistogram/Histogram.java
index 932518a..5778e99 100644
--- a/src/main/java/org/HdrHistogram/Histogram.java
+++ b/src/main/java/org/HdrHistogram/Histogram.java
@@ -10,7 +10,6 @@ package org.HdrHistogram;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.nio.ByteBuffer;
-import java.nio.LongBuffer;
 import java.util.Arrays;
 import java.util.zip.DataFormatException;
 
@@ -245,7 +244,7 @@ public class Histogram extends AbstractHistogram {
      */
     public static Histogram decodeFromByteBuffer(final ByteBuffer buffer,
                                                  final long minBarForHighestTrackableValue) {
-        return (Histogram) decodeFromByteBuffer(buffer, Histogram.class, minBarForHighestTrackableValue);
+        return decodeFromByteBuffer(buffer, Histogram.class, minBarForHighestTrackableValue);
     }
 
     /**
@@ -256,7 +255,8 @@ public class Histogram extends AbstractHistogram {
      * @throws DataFormatException on error parsing/decompressing the buffer
      */
     public static Histogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
-                                                           final long minBarForHighestTrackableValue) throws DataFormatException {
+                                                           final long minBarForHighestTrackableValue)
+            throws DataFormatException {
         return decodeFromCompressedByteBuffer(buffer, Histogram.class, minBarForHighestTrackableValue);
     }
 
@@ -265,8 +265,18 @@ public class Histogram extends AbstractHistogram {
         o.defaultReadObject();
     }
 
-    @Override
-    synchronized void fillCountsArrayFromBuffer(final ByteBuffer buffer, final int length) {
-        buffer.asLongBuffer().get(counts, 0, length);
+    /**
+     * Construct a new Histogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A Histogream decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static Histogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
     }
 }
diff --git a/src/main/java/org/HdrHistogram/HistogramLogProcessor.java b/src/main/java/org/HdrHistogram/HistogramLogProcessor.java
index f4975f2..3f541c3 100644
--- a/src/main/java/org/HdrHistogram/HistogramLogProcessor.java
+++ b/src/main/java/org/HdrHistogram/HistogramLogProcessor.java
@@ -246,10 +246,10 @@ public class HistogramLogProcessor extends Thread {
         boolean timeIntervalLogLegendWritten = false;
         boolean movingWindowLogLegendWritten = false;
 
-        Queue<EncodableHistogram> movingWindowQueue = new LinkedList<EncodableHistogram>();
+        Queue<EncodableHistogram> movingWindowQueue = new LinkedList<>();
 
         if (config.listTags) {
-            Set<String> tags = new TreeSet<String>();
+            Set<String> tags = new TreeSet<>();
             EncodableHistogram histogram;
             boolean nullTagFound = false;
             while ((histogram = getIntervalHistogram()) != null) {
@@ -347,22 +347,24 @@ public class HistogramLogProcessor extends Thread {
                 long windowCutOffTimeStamp = intervalHistogram.getEndTimeStamp() - config.movingWindowLengthInMsec;
                 // handle moving window:
                 if (config.movingWindow) {
-
                     // Add the current interval histogram to the moving window sums:
-                    if (movingWindowSumHistogram instanceof DoubleHistogram) {
+                    if ((movingWindowSumHistogram instanceof DoubleHistogram) &&
+                            (intervalHistogram instanceof DoubleHistogram)){
                         ((DoubleHistogram) movingWindowSumHistogram).add((DoubleHistogram) intervalHistogram);
-                    } else {
+                    } else if ((movingWindowSumHistogram instanceof Histogram) &&
+                            (intervalHistogram instanceof Histogram)){
                         ((Histogram) movingWindowSumHistogram).add((Histogram) intervalHistogram);
                     }
                     // Remove previous, now-out-of-window interval histograms from moving window:
-                    while ((movingWindowQueue.peek() != null) &&
-                            (movingWindowQueue.peek().getEndTimeStamp() <= windowCutOffTimeStamp)) {
+                    EncodableHistogram head;
+                    while (((head = movingWindowQueue.peek()) != null) &&
+                            (head.getEndTimeStamp() <= windowCutOffTimeStamp)) {
                         EncodableHistogram prevHist = movingWindowQueue.remove();
                         if (movingWindowSumHistogram instanceof DoubleHistogram) {
                             if (prevHist != null) {
                                 ((DoubleHistogram) movingWindowSumHistogram).subtract((DoubleHistogram) prevHist);
                             }
-                        } else {
+                        } else if (movingWindowSumHistogram instanceof Histogram) {
                             if (prevHist != null) {
                                 ((Histogram) movingWindowSumHistogram).subtract((Histogram) prevHist);
                             }
diff --git a/src/main/java/org/HdrHistogram/HistogramLogReader.java b/src/main/java/org/HdrHistogram/HistogramLogReader.java
index 25fb317..6f300a0 100644
--- a/src/main/java/org/HdrHistogram/HistogramLogReader.java
+++ b/src/main/java/org/HdrHistogram/HistogramLogReader.java
@@ -54,8 +54,7 @@ import java.util.zip.DataFormatException;
 public class HistogramLogReader implements Closeable {
 
     private final HistogramLogScanner scanner;
-    private final HistogramLogScanner.EventHandler handler = new HistogramLogScanner.EventHandler()
-    {
+    private final HistogramLogScanner.EventHandler handler = new HistogramLogScanner.EventHandler() {
         @Override
         public boolean onComment(String comment)
         {
@@ -119,12 +118,9 @@ public class HistogramLogReader implements Closeable {
                 return true;
             }
             EncodableHistogram histogram;
-            try
-            {
+            try {
                 histogram = lazyReader.read();
-            }
-            catch (DataFormatException e)
-            {
+            } catch (DataFormatException e) {
                 // stop after exception
                 return true;
             }
@@ -145,10 +141,11 @@ public class HistogramLogReader implements Closeable {
                 return true;
             }
             // rethrow
-            if (t instanceof RuntimeException)
-                throw (RuntimeException)t;
-            else
+            if (t instanceof RuntimeException) {
+                throw (RuntimeException) t;
+            } else {
                 throw new RuntimeException(t);
+            }
         }
     };
     
diff --git a/src/main/java/org/HdrHistogram/HistogramLogScanner.java b/src/main/java/org/HdrHistogram/HistogramLogScanner.java
index 0f86c2e..a848a2b 100644
--- a/src/main/java/org/HdrHistogram/HistogramLogScanner.java
+++ b/src/main/java/org/HdrHistogram/HistogramLogScanner.java
@@ -16,16 +16,14 @@ import java.util.zip.DataFormatException;
 public class HistogramLogScanner implements Closeable {
 
     // can't use lambdas, and anyway we need to let the handler take the exception
-    public interface EncodableHistogramSupplier
-    {
+    public interface EncodableHistogramSupplier {
         EncodableHistogram read() throws DataFormatException;
     }
 
     /**
      * Handles log events, return true to stop processing.
      */
-    public interface EventHandler
-    {
+    public interface EventHandler {
         boolean onComment(String comment);
         boolean onBaseTime(double secondsSinceEpoch);
         boolean onStartTime(double secondsSinceEpoch);
@@ -38,7 +36,7 @@ public class HistogramLogScanner implements Closeable {
          * @param timestamp logged timestamp
          * @param length logged interval length
          * @param lazyReader to be called if the histogram needs to be deserialized, given the tag/timestamp etc.
-         * @return
+         * @return true to stop processing, false to continue.
          */
         boolean onHistogram(String tag, double timestamp, double length, EncodableHistogramSupplier lazyReader);
         boolean onException(Throwable t);
@@ -63,8 +61,9 @@ public class HistogramLogScanner implements Closeable {
         public EncodableHistogram read() throws DataFormatException
         {
             // prevent double calls to this method
-            if (gotIt)
+            if (gotIt) {
                 throw new IllegalStateException();
+            }
             gotIt = true;
             
             final String compressedPayloadString = scanner.next();
@@ -174,17 +173,18 @@ public class HistogramLogScanner implements Closeable {
                 scanner.nextDouble(); // Skip maxTime field, as max time can be deduced from the histogram.
                 
                 lazyReader.allowGet();
-                if (handler.onHistogram(tagString, logTimeStampInSec, intervalLengthSec, lazyReader))
+                if (handler.onHistogram(tagString, logTimeStampInSec, intervalLengthSec, lazyReader)) {
                     return;
+                }
 
             } catch (Throwable ex) {
-                if (handler.onException(ex))
+                if (handler.onException(ex)) {
                     return;
+                }
             } finally {
                 scanner.nextLine(); // Move to next line.
             }
         }
-        return;
     }
 
     /**
diff --git a/src/main/java/org/HdrHistogram/HistogramLogWriter.java b/src/main/java/org/HdrHistogram/HistogramLogWriter.java
index d1dcd9d..67ce7fa 100644
--- a/src/main/java/org/HdrHistogram/HistogramLogWriter.java
+++ b/src/main/java/org/HdrHistogram/HistogramLogWriter.java
@@ -5,10 +5,8 @@ import java.io.FileNotFoundException;
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
 import java.util.Arrays;
 import java.util.Date;
-import java.util.IllegalFormatException;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -16,7 +14,6 @@ import java.util.zip.Deflater;
 
 import static java.nio.ByteOrder.BIG_ENDIAN;
 
-
 /**
  * A histogram log writer.
  * <p>
diff --git a/src/main/java/org/HdrHistogram/IntCountsHistogram.java b/src/main/java/org/HdrHistogram/IntCountsHistogram.java
index 4213019..fd95ff7 100644
--- a/src/main/java/org/HdrHistogram/IntCountsHistogram.java
+++ b/src/main/java/org/HdrHistogram/IntCountsHistogram.java
@@ -10,7 +10,6 @@ package org.HdrHistogram;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.nio.ByteBuffer;
-import java.nio.IntBuffer;
 import java.util.Arrays;
 import java.util.zip.DataFormatException;
 
@@ -53,7 +52,7 @@ public class IntCountsHistogram extends AbstractHistogram {
         long currentCount = counts[normalizedIndex];
         long newCount = (currentCount + value);
         if ((newCount < Integer.MIN_VALUE) || (newCount > Integer.MAX_VALUE)) {
-            throw new IllegalArgumentException("would overflow integer count");
+            throw new IllegalStateException("would overflow integer count");
         }
         counts[normalizedIndex] = (int) newCount;
     }
@@ -66,7 +65,7 @@ public class IntCountsHistogram extends AbstractHistogram {
     @Override
     void setCountAtNormalizedIndex(int index, long value) {
         if ((value < 0) || (value > Integer.MAX_VALUE)) {
-            throw new IllegalArgumentException("would overflow short integer count");
+            throw new IllegalStateException("would overflow integer count");
         }
         counts[index] = (int) value;
     }
@@ -227,8 +226,7 @@ public class IntCountsHistogram extends AbstractHistogram {
      */
     public static IntCountsHistogram decodeFromByteBuffer(final ByteBuffer buffer,
                                                     final long minBarForHighestTrackableValue) {
-        return (IntCountsHistogram) decodeFromByteBuffer(buffer, IntCountsHistogram.class,
-                minBarForHighestTrackableValue);
+        return decodeFromByteBuffer(buffer, IntCountsHistogram.class, minBarForHighestTrackableValue);
     }
 
     /**
@@ -239,17 +237,28 @@ public class IntCountsHistogram extends AbstractHistogram {
      * @throws DataFormatException on error parsing/decompressing the buffer
      */
     public static IntCountsHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
-                                                              final long minBarForHighestTrackableValue) throws DataFormatException {
+                                                              final long minBarForHighestTrackableValue)
+            throws DataFormatException {
         return decodeFromCompressedByteBuffer(buffer, IntCountsHistogram.class, minBarForHighestTrackableValue);
     }
 
+    /**
+     * Construct a new IntCountsHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A IntCountsHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static IntCountsHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
+    }
+
     private void readObject(final ObjectInputStream o)
             throws IOException, ClassNotFoundException {
         o.defaultReadObject();
     }
-
-    @Override
-    synchronized void fillCountsArrayFromBuffer(final ByteBuffer buffer, final int length) {
-        buffer.asIntBuffer().get(counts, 0, length);
-    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/HdrHistogram/PackedConcurrentDoubleHistogram.java b/src/main/java/org/HdrHistogram/PackedConcurrentDoubleHistogram.java
new file mode 100644
index 0000000..f301df4
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/PackedConcurrentDoubleHistogram.java
@@ -0,0 +1,160 @@
+/**
+ * Written by Gil Tene of Azul Systems, and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * @author Gil Tene
+ */
+
+package org.HdrHistogram;
+
+import java.nio.ByteBuffer;
+import java.util.zip.DataFormatException;
+
+/**
+ * <h3>A floating point values High Dynamic Range (HDR) Histogram that uses a packed internal representation and
+ * supports safe concurrent recording operations.</h3>
+ * <p>
+ * A {@link PackedConcurrentDoubleHistogram} is a variant of {@link DoubleHistogram} that guarantees
+ * lossless recording of values into the histogram even when the histogram is updated by multiple threads, and
+ * supports auto-resize and auto-ranging operations that may occur concurrently as a result of recording operations.
+ * <p>
+ * {@link PackedConcurrentDoubleHistogram} tracks value counts in a packed internal representation optimized
+ * for typical histogram recoded values are sparse in the value range and tend to be incremented in small unit counts.
+ * This packed representation tends to require significantly smaller amounts of stoarge when compared to unpacked
+ * representations, but can incur additional recording cost due to resizing and repacking operations that may
+ * occur as previously unrecorded values are encountered.
+ * <p>
+ * It is important to note that concurrent recording, auto-sizing, and value shifting are the only thread-safe behaviors
+ * provided by {@link PackedConcurrentDoubleHistogram}, and that it is not otherwise synchronized. Specifically, {@link
+ * PackedConcurrentDoubleHistogram} provides no implicit synchronization that would prevent the contents of the histogram
+ * from changing during queries, iterations, copies, or addition operations on the histogram. Callers wishing to make
+ * potentially concurrent, multi-threaded updates that would safely work in the presence of queries, copies, or
+ * additions of histogram objects should either take care to externally synchronize and/or order their access,
+ * use the {@link DoubleRecorder} or {@link SingleWriterDoubleRecorder} which are intended for this purpose.
+ * <p>
+ * {@link PackedConcurrentDoubleHistogram} supports the recording and analyzing sampled data value counts across a
+ * configurable dynamic range of floating point (double) values, with configurable value precision within the range.
+ * Dynamic range is expressed as a ratio between the highest and lowest non-zero values trackable within the histogram
+ * at any given time. Value precision is expressed as the number of significant [decimal] digits in the value recording,
+ * and provides control over value quantization behavior across the value range and the subsequent value resolution at
+ * any given level.
+ * <p>
+ * Auto-ranging: Unlike integer value based histograms, the specific value range tracked by a {@link
+ * PackedConcurrentDoubleHistogram} is not specified upfront. Only the dynamic range of values that the histogram can cover is
+ * (optionally) specified. E.g. When a {@link PackedConcurrentDoubleHistogram} is created to track a dynamic range of
+ * 3600000000000 (enough to track values from a nanosecond to an hour), values could be recorded into into it in any
+ * consistent unit of time as long as the ratio between the highest and lowest non-zero values stays within the
+ * specified dynamic range, so recording in units of nanoseconds (1.0 thru 3600000000000.0), milliseconds (0.000001
+ * thru 3600000.0) seconds (0.000000001 thru 3600.0), hours (1/3.6E12 thru 1.0) will all work just as well.
+ * <p>
+ * Auto-resizing: When constructed with no specified dynamic range (or when auto-resize is turned on with {@link
+ * PackedConcurrentDoubleHistogram#setAutoResize}) a {@link PackedConcurrentDoubleHistogram} will auto-resize its dynamic range to
+ * include recorded values as they are encountered. Note that recording calls that cause auto-resizing may take
+ * longer to execute, as resizing incurs allocation and copying of internal data structures.
+ * <p>
+ * Attempts to record non-zero values that range outside of the specified dynamic range (or exceed the limits of
+ * of dynamic range when auto-resizing) may results in {@link ArrayIndexOutOfBoundsException} exceptions, either
+ * due to overflow or underflow conditions. These exceptions will only be thrown if recording the value would have
+ * resulted in discarding or losing the required value precision of values already recorded in the histogram.
+ * <p>
+ * See package description for {@link org.HdrHistogram} for details.
+ */
+
+public class PackedConcurrentDoubleHistogram extends ConcurrentDoubleHistogram {
+
+    /**
+     * Construct a new auto-resizing DoubleHistogram using a precision stated as a number of significant decimal
+     * digits.
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant decimal
+     *                                       digits to which the histogram will maintain value resolution and
+     *                                       separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedConcurrentDoubleHistogram(final int numberOfSignificantValueDigits) {
+        this(2, numberOfSignificantValueDigits);
+        setAutoResize(true);
+    }
+
+    /**
+     * Construct a new DoubleHistogram with the specified dynamic range (provided in {@code highestToLowestValueRatio})
+     * and using a precision stated as a number of significant decimal digits.
+     *
+     * @param highestToLowestValueRatio      specifies the dynamic range to use
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant decimal
+     *                                       digits to which the histogram will maintain value resolution and
+     *                                       separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedConcurrentDoubleHistogram(final long highestToLowestValueRatio, final int numberOfSignificantValueDigits) {
+        this(highestToLowestValueRatio, numberOfSignificantValueDigits, PackedConcurrentHistogram.class);
+    }
+
+    /**
+     * Construct a {@link PackedConcurrentDoubleHistogram} with the same range settings as a given source,
+     * duplicating the source's start/end timestamps (but NOT it's contents)
+     * @param source The source histogram to duplicate
+     */
+    public PackedConcurrentDoubleHistogram(final DoubleHistogram source) {
+        super(source);
+    }
+
+    PackedConcurrentDoubleHistogram(final long highestToLowestValueRatio,
+                                    final int numberOfSignificantValueDigits,
+                                    final Class<? extends AbstractHistogram> internalCountsHistogramClass) {
+        super(highestToLowestValueRatio, numberOfSignificantValueDigits, internalCountsHistogramClass);
+    }
+
+    PackedConcurrentDoubleHistogram(final long highestToLowestValueRatio,
+                              final int numberOfSignificantValueDigits,
+                              final Class<? extends AbstractHistogram> internalCountsHistogramClass,
+                              AbstractHistogram internalCountsHistogram) {
+        super(
+                highestToLowestValueRatio,
+                numberOfSignificantValueDigits,
+                internalCountsHistogramClass,
+                internalCountsHistogram
+        );
+    }
+
+    /**
+     * Construct a new ConcurrentDoubleHistogram by decoding it from a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestToLowestValueRatio Force highestTrackableValue to be set at least this high
+     * @return The newly constructed ConcurrentDoubleHistogram
+     */
+    public static PackedConcurrentDoubleHistogram decodeFromByteBuffer(
+            final ByteBuffer buffer,
+            final long minBarForHighestToLowestValueRatio) {
+        try {
+            int cookie = buffer.getInt();
+            if (!isNonCompressedDoubleHistogramCookie(cookie)) {
+                throw new IllegalArgumentException("The buffer does not contain a DoubleHistogram");
+            }
+            PackedConcurrentDoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                    PackedConcurrentDoubleHistogram.class, PackedConcurrentHistogram.class,
+                    minBarForHighestToLowestValueRatio);
+            return histogram;
+        } catch (DataFormatException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    /**
+     * Construct a new ConcurrentDoubleHistogram by decoding it from a compressed form in a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestToLowestValueRatio Force highestTrackableValue to be set at least this high
+     * @return The newly constructed ConcurrentDoubleHistogram
+     * @throws DataFormatException on error parsing/decompressing the buffer
+     */
+    public static PackedConcurrentDoubleHistogram decodeFromCompressedByteBuffer(
+            final ByteBuffer buffer,
+            final long minBarForHighestToLowestValueRatio) throws DataFormatException {
+        int cookie = buffer.getInt();
+        if (!isCompressedDoubleHistogramCookie(cookie)) {
+            throw new IllegalArgumentException("The buffer does not contain a compressed DoubleHistogram");
+        }
+        PackedConcurrentDoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                PackedConcurrentDoubleHistogram.class, PackedConcurrentHistogram.class,
+                minBarForHighestToLowestValueRatio);
+        return histogram;
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/PackedConcurrentHistogram.java b/src/main/java/org/HdrHistogram/PackedConcurrentHistogram.java
new file mode 100644
index 0000000..ef49fa8
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/PackedConcurrentHistogram.java
@@ -0,0 +1,309 @@
+/**
+ * Written by Gil Tene of Azul Systems, and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * @author Gil Tene
+ */
+
+package org.HdrHistogram;
+
+import org.HdrHistogram.packedarray.ConcurrentPackedLongArray;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.util.zip.DataFormatException;
+
+/**
+ * <h3>An integer values High Dynamic Range (HDR) Histogram that uses a packed internal representation
+ * and supports safe concurrent recording operations.</h3>
+ * A {@link PackedConcurrentHistogram} guarantees lossless recording of values into the histogram even when the
+ * histogram is updated by multiple threads, and supports auto-resize and shift operations that may
+ * result from or occur concurrently with other recording operations.
+ * <p>
+ * {@link PackedConcurrentHistogram} tracks value counts in a packed internal representation optimized
+ * for typical histogram recoded values are sparse in the value range and tend to be incremented in small unit counts.
+ * This packed representation tends to require significantly smaller amounts of stoarge when compared to unpacked
+ * representations, but can incur additional recording cost due to resizing and repacking operations that may
+ * occur as previously unrecorded values are encountered.
+ * <p>
+ * It is important to note that concurrent recording, auto-sizing, and value shifting are the only thread-safe
+ * behaviors provided by {@link PackedConcurrentHistogram}, and that it is not otherwise synchronized. Specifically,
+ * {@link PackedConcurrentHistogram} provides no implicit synchronization that would prevent the contents of the
+ * histogram from changing during queries, iterations, copies, or addition operations on the histogram. Callers
+ * wishing to make potentially concurrent, multi-threaded updates that would safely work in the presence of
+ * queries, copies, or additions of histogram objects should either take care to externally synchronize and/or
+ * order their access, use {@link Recorder} or {@link SingleWriterRecorder} which are intended for
+ * this purpose.
+ * <p>
+ * Auto-resizing: When constructed with no specified value range range (or when auto-resize is turned on with {@link
+ * Histogram#setAutoResize}) a {@link PackedConcurrentHistogram} will auto-resize its dynamic range to include recorded
+ * values as they are encountered. Note that recording calls that cause auto-resizing may take longer to execute, as
+ * resizing incurs allocation and copying of internal data structures.
+ * <p>
+ * See package description for {@link org.HdrHistogram} for details.
+ */
+
+public class PackedConcurrentHistogram extends ConcurrentHistogram {
+
+    @Override
+    ConcurrentArrayWithNormalizingOffset allocateArray(int length, int normalizingIndexOffset) {
+        return new ConcurrentPackedArrayWithNormalizingOffset(length, normalizingIndexOffset);
+    }
+
+    @Override
+    void clearCounts() {
+        try {
+            wrp.readerLock();
+            assert (countsArrayLength == activeCounts.length());
+            assert (countsArrayLength == inactiveCounts.length());
+            for (int i = 0; i < activeCounts.length(); i++) {
+                activeCounts.lazySet(i, 0);
+                inactiveCounts.lazySet(i, 0);
+            }
+            totalCountUpdater.set(this, 0);
+        } finally {
+            wrp.readerUnlock();
+        }
+    }
+
+    @Override
+    public PackedConcurrentHistogram copy() {
+        PackedConcurrentHistogram copy = new PackedConcurrentHistogram(this);
+        copy.add(this);
+        return copy;
+    }
+
+    @Override
+    public PackedConcurrentHistogram copyCorrectedForCoordinatedOmission(final long expectedIntervalBetweenValueSamples) {
+        PackedConcurrentHistogram toHistogram = new PackedConcurrentHistogram(this);
+        toHistogram.addWhileCorrectingForCoordinatedOmission(this, expectedIntervalBetweenValueSamples);
+        return toHistogram;
+    }
+
+    @Override
+    public long getTotalCount() {
+        return totalCountUpdater.get(this);
+    }
+
+    @Override
+    void setTotalCount(final long totalCount) {
+        totalCountUpdater.set(this, totalCount);
+    }
+
+    @Override
+    void incrementTotalCount() {
+        totalCountUpdater.incrementAndGet(this);
+    }
+
+    @Override
+    void addToTotalCount(final long value) {
+        totalCountUpdater.addAndGet(this, value);
+    }
+
+
+    @Override
+    int _getEstimatedFootprintInBytes() {
+        try {
+            wrp.readerLock();
+            return 128 + activeCounts.getEstimatedFootprintInBytes() + inactiveCounts.getEstimatedFootprintInBytes();
+        } finally {
+            wrp.readerUnlock();
+        }
+    }
+
+    /**
+     * Construct an auto-resizing ConcurrentHistogram with a lowest discernible value of 1 and an auto-adjusting
+     * highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedConcurrentHistogram(final int numberOfSignificantValueDigits) {
+        this(1, 2, numberOfSignificantValueDigits);
+        setAutoResize(true);
+    }
+
+    /**
+     * Construct a ConcurrentHistogram given the Highest value to be tracked and a number of significant decimal
+     * digits. The histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
+     *
+     * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
+     *                              integer that is {@literal >=} 2.
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedConcurrentHistogram(final long highestTrackableValue, final int numberOfSignificantValueDigits) {
+        this(1, highestTrackableValue, numberOfSignificantValueDigits);
+    }
+
+    /**
+     * Construct a ConcurrentHistogram given the Lowest and Highest values to be tracked and a number of significant
+     * decimal digits. Providing a lowestDiscernibleValue is useful is situations where the units used
+     * for the histogram's values are much smaller that the minimal accuracy required. E.g. when tracking
+     * time values stated in nanosecond units, where the minimal accuracy required is a microsecond, the
+     * proper value for lowestDiscernibleValue would be 1000.
+     *
+     * @param lowestDiscernibleValue The lowest value that can be tracked (distinguished from 0) by the histogram.
+     *                               Must be a positive integer that is {@literal >=} 1. May be internally rounded
+     *                               down to nearest power of 2.
+     * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
+     *                              integer that is {@literal >=} (2 * lowestDiscernibleValue).
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedConcurrentHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
+                                     final int numberOfSignificantValueDigits) {
+        this(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits,
+                true);
+    }
+
+    /**
+     * Construct a histogram with the same range settings as a given source histogram,
+     * duplicating the source's start/end timestamps (but NOT it's contents)
+     * @param source The source histogram to duplicate
+     */
+    public PackedConcurrentHistogram(final AbstractHistogram source) {
+        this(source, true);
+    }
+
+
+    PackedConcurrentHistogram(final AbstractHistogram source, boolean allocateCountsArray) {
+        super(source,false);
+        if (allocateCountsArray) {
+            activeCounts = new ConcurrentPackedArrayWithNormalizingOffset(countsArrayLength, 0);
+            inactiveCounts = new ConcurrentPackedArrayWithNormalizingOffset(countsArrayLength, 0);
+        }
+        wordSizeInBytes = 8;
+    }
+
+    PackedConcurrentHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
+                              final int numberOfSignificantValueDigits, boolean allocateCountsArray) {
+        super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits,
+                false);
+        if (allocateCountsArray) {
+            activeCounts = new ConcurrentPackedArrayWithNormalizingOffset(countsArrayLength, 0);
+            inactiveCounts = new ConcurrentPackedArrayWithNormalizingOffset(countsArrayLength, 0);
+        }
+        wordSizeInBytes = 8;
+    }
+
+    /**
+     * Construct a new histogram by decoding it from a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
+     * @return The newly constructed histogram
+     */
+    public static PackedConcurrentHistogram decodeFromByteBuffer(final ByteBuffer buffer,
+                                                                 final long minBarForHighestTrackableValue) {
+        return decodeFromByteBuffer(buffer, PackedConcurrentHistogram.class, minBarForHighestTrackableValue);
+    }
+
+    /**
+     * Construct a new histogram by decoding it from a compressed form in a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
+     * @return The newly constructed histogram
+     * @throws DataFormatException on error parsing/decompressing the buffer
+     */
+    public static PackedConcurrentHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
+                                                                           final long minBarForHighestTrackableValue)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(buffer, PackedConcurrentHistogram.class, minBarForHighestTrackableValue);
+    }
+
+    /**
+     * Construct a new ConcurrentHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A ConcurrentHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static PackedConcurrentHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
+    }
+
+    private void readObject(final ObjectInputStream o)
+            throws IOException, ClassNotFoundException {
+        o.defaultReadObject();
+        wrp = new WriterReaderPhaser();
+    }
+
+    @Override
+    synchronized void fillBufferFromCountsArray(final ByteBuffer buffer) {
+        try {
+            wrp.readerLock();
+            super.fillBufferFromCountsArray(buffer);
+        } finally {
+            wrp.readerUnlock();
+        }
+    }
+
+    static class ConcurrentPackedArrayWithNormalizingOffset
+            implements ConcurrentArrayWithNormalizingOffset, Serializable {
+
+        private ConcurrentPackedLongArray packedCounts;
+
+        private int normalizingIndexOffset;
+        private double doubleToIntegerValueConversionRatio;
+
+        ConcurrentPackedArrayWithNormalizingOffset(int length, int normalizingIndexOffset) {
+            packedCounts = new ConcurrentPackedLongArray(length);
+            this.normalizingIndexOffset = normalizingIndexOffset;
+        }
+
+        public int getNormalizingIndexOffset() {
+            return normalizingIndexOffset;
+        }
+
+        public void setNormalizingIndexOffset(int normalizingIndexOffset) {
+            this.normalizingIndexOffset = normalizingIndexOffset;
+        }
+
+        public double getDoubleToIntegerValueConversionRatio() {
+            return doubleToIntegerValueConversionRatio;
+        }
+
+        public void setDoubleToIntegerValueConversionRatio(double doubleToIntegerValueConversionRatio) {
+            this.doubleToIntegerValueConversionRatio = doubleToIntegerValueConversionRatio;
+        }
+
+        @Override
+        public long get(int index) {
+            return packedCounts.get(index);
+        }
+
+        @Override
+        public void atomicIncrement(int index) {
+            packedCounts.increment(index);
+        }
+
+        @Override
+        public void atomicAdd(int index, long valueToAdd) {
+            packedCounts.add(index, valueToAdd);
+        }
+
+        @Override
+        public void lazySet(int index, long newValue) {
+            packedCounts.set(index, newValue);
+        }
+
+        @Override
+        public int length() {
+            return packedCounts.length();
+        }
+
+        @Override
+        public int getEstimatedFootprintInBytes() {
+            return 128 + (8 * packedCounts.getPhysicalLength());
+        }
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/PackedDoubleHistogram.java b/src/main/java/org/HdrHistogram/PackedDoubleHistogram.java
new file mode 100644
index 0000000..8c86456
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/PackedDoubleHistogram.java
@@ -0,0 +1,152 @@
+/**
+ * Written by Gil Tene of Azul Systems, and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * @author Gil Tene
+ */
+
+package org.HdrHistogram;
+
+import java.nio.ByteBuffer;
+import java.util.zip.DataFormatException;
+
+/**
+ * <h3>A floating point values High Dynamic Range (HDR) Histogram that uses a packed internal representation</h3>
+ * <p>
+ * It is important to note that {@link PackedDoubleHistogram} is not thread-safe, and does not support safe concurrent
+ * recording by multiple threads. If concurrent operation is required, consider usings
+ * {@link PackedConcurrentDoubleHistogram}, or (recommended) {@link DoubleRecorder} or
+ * {@link SingleWriterDoubleRecorder} which are intended for this purpose.
+ * <p>
+ * {@link PackedDoubleHistogram} tracks value counts in a packed internal representation optimized
+ * for typical histogram recoded values are sparse in the value range and tend to be incremented in small unit counts.
+ * This packed representation tends to require significantly smaller amounts of stoarge when compared to unpacked
+ * representations, but can incur additional recording cost due to resizing and repacking operations that may
+ * occur as previously unrecorded values are encountered.
+ * <p>
+ * {@link PackedDoubleHistogram} supports the recording and analyzing sampled data value counts across a
+ * configurable dynamic range of floating point (double) values, with configurable value precision within the range.
+ * Dynamic range is expressed as a ratio between the highest and lowest non-zero values trackable within the histogram
+ * at any given time. Value precision is expressed as the number of significant [decimal] digits in the value recording,
+ * and provides control over value quantization behavior across the value range and the subsequent value resolution at
+ * any given level.
+ * <p>
+ * Auto-ranging: Unlike integer value based histograms, the specific value range tracked by a {@link
+ * PackedDoubleHistogram} is not specified upfront. Only the dynamic range of values that the histogram can cover is
+ * (optionally) specified. E.g. When a {@link PackedDoubleHistogram} is created to track a dynamic range of
+ * 3600000000000 (enough to track values from a nanosecond to an hour), values could be recorded into into it in any
+ * consistent unit of time as long as the ratio between the highest and lowest non-zero values stays within the
+ * specified dynamic range, so recording in units of nanoseconds (1.0 thru 3600000000000.0), milliseconds (0.000001
+ * thru 3600000.0) seconds (0.000000001 thru 3600.0), hours (1/3.6E12 thru 1.0) will all work just as well.
+ * <p>
+ * Auto-resizing: When constructed with no specified dynamic range (or when auto-resize is turned on with {@link
+ * DoubleHistogram#setAutoResize}) a {@link DoubleHistogram} will auto-resize its dynamic range to
+ * include recorded values as they are encountered. Note that recording calls that cause auto-resizing may take
+ * longer to execute, as resizing incurs allocation and copying of internal data structures.
+ * <p>
+ * Attempts to record non-zero values that range outside of the specified dynamic range (or exceed the limits of
+ * of dynamic range when auto-resizing) may results in {@link ArrayIndexOutOfBoundsException} exceptions, either
+ * due to overflow or underflow conditions. These exceptions will only be thrown if recording the value would have
+ * resulted in discarding or losing the required value precision of values already recorded in the histogram.
+ * <p>
+ * See package description for {@link org.HdrHistogram} for details.
+ */
+
+public class PackedDoubleHistogram extends DoubleHistogram {
+
+    /**
+     * Construct a new auto-resizing DoubleHistogram using a precision stated as a number of significant decimal
+     * digits.
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant decimal
+     *                                       digits to which the histogram will maintain value resolution and
+     *                                       separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedDoubleHistogram(final int numberOfSignificantValueDigits) {
+        this(2, numberOfSignificantValueDigits);
+        setAutoResize(true);
+    }
+
+    /**
+     * Construct a new DoubleHistogram with the specified dynamic range (provided in {@code highestToLowestValueRatio})
+     * and using a precision stated as a number of significant decimal digits.
+     *
+     * @param highestToLowestValueRatio      specifies the dynamic range to use
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant decimal
+     *                                       digits to which the histogram will maintain value resolution and
+     *                                       separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedDoubleHistogram(final long highestToLowestValueRatio, final int numberOfSignificantValueDigits) {
+        this(highestToLowestValueRatio, numberOfSignificantValueDigits, PackedHistogram.class);
+    }
+
+    /**
+     * Construct a {@link PackedDoubleHistogram} with the same range settings as a given source,
+     * duplicating the source's start/end timestamps (but NOT it's contents)
+     * @param source The source histogram to duplicate
+     */
+    public PackedDoubleHistogram(final DoubleHistogram source) {
+        super(source);
+    }
+
+    PackedDoubleHistogram(final long highestToLowestValueRatio,
+                          final int numberOfSignificantValueDigits,
+                          final Class<? extends AbstractHistogram> internalCountsHistogramClass) {
+        super(highestToLowestValueRatio, numberOfSignificantValueDigits, internalCountsHistogramClass);
+    }
+
+    PackedDoubleHistogram(final long highestToLowestValueRatio,
+                          final int numberOfSignificantValueDigits,
+                          final Class<? extends AbstractHistogram> internalCountsHistogramClass,
+                          AbstractHistogram internalCountsHistogram) {
+        super(
+                highestToLowestValueRatio,
+                numberOfSignificantValueDigits,
+                internalCountsHistogramClass,
+                internalCountsHistogram
+        );
+    }
+
+    /**
+     * Construct a new ConcurrentDoubleHistogram by decoding it from a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestToLowestValueRatio Force highestTrackableValue to be set at least this high
+     * @return The newly constructed ConcurrentDoubleHistogram
+     */
+    public static PackedDoubleHistogram decodeFromByteBuffer(
+            final ByteBuffer buffer,
+            final long minBarForHighestToLowestValueRatio) {
+        try {
+            int cookie = buffer.getInt();
+            if (!isNonCompressedDoubleHistogramCookie(cookie)) {
+                throw new IllegalArgumentException("The buffer does not contain a DoubleHistogram");
+            }
+            PackedDoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                    PackedDoubleHistogram.class, PackedHistogram.class,
+                    minBarForHighestToLowestValueRatio);
+            return histogram;
+        } catch (DataFormatException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    /**
+     * Construct a new ConcurrentDoubleHistogram by decoding it from a compressed form in a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestToLowestValueRatio Force highestTrackableValue to be set at least this high
+     * @return The newly constructed ConcurrentDoubleHistogram
+     * @throws DataFormatException on error parsing/decompressing the buffer
+     */
+    public static PackedDoubleHistogram decodeFromCompressedByteBuffer(
+            final ByteBuffer buffer,
+            final long minBarForHighestToLowestValueRatio) throws DataFormatException {
+        int cookie = buffer.getInt();
+        if (!isCompressedDoubleHistogramCookie(cookie)) {
+            throw new IllegalArgumentException("The buffer does not contain a compressed DoubleHistogram");
+        }
+        PackedDoubleHistogram histogram = constructHistogramFromBuffer(cookie, buffer,
+                PackedDoubleHistogram.class, PackedHistogram.class,
+                minBarForHighestToLowestValueRatio);
+        return histogram;
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/PackedHistogram.java b/src/main/java/org/HdrHistogram/PackedHistogram.java
new file mode 100644
index 0000000..5bf49cb
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/PackedHistogram.java
@@ -0,0 +1,235 @@
+/**
+ * Written by Gil Tene of Azul Systems, and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * @author Gil Tene
+ */
+
+package org.HdrHistogram;
+
+import org.HdrHistogram.packedarray.PackedLongArray;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.nio.ByteBuffer;
+import java.util.zip.DataFormatException;
+
+/**
+ * <h3>A High Dynamic Range (HDR) Histogram that uses a packed internal representation</h3>
+ * <p>
+ * {@link PackedHistogram} supports the recording and analyzing sampled data value counts across a configurable
+ * integer value range with configurable value precision within the range. Value precision is expressed as the
+ * number of significant digits in the value recording, and provides control over value quantization behavior
+ * across the value range and the subsequent value resolution at any given level.
+ * <p>
+ * {@link PackedHistogram} tracks value counts in a packed internal representation optimized
+ * for typical histogram recoded values are sparse in the value range and tend to be incremented in small unit counts.
+ * This packed representation tends to require significantly smaller amounts of stoarge when compared to unpacked
+ * representations, but can incur additional recording cost due to resizing and repacking operations that may
+ * occur as previously unrecorded values are encountered.
+ * <p>
+ * For example, a {@link PackedHistogram} could be configured to track the counts of observed integer values between 0 and
+ * 3,600,000,000,000 while maintaining a value precision of 3 significant digits across that range. Value quantization
+ * within the range will thus be no larger than 1/1,000th (or 0.1%) of any value. This example Histogram could
+ * be used to track and analyze the counts of observed response times ranging between 1 nanosecond and 1 hour
+ * in magnitude, while maintaining a value resolution of 1 microsecond up to 1 millisecond, a resolution of
+ * 1 millisecond (or better) up to one second, and a resolution of 1 second (or better) up to 1,000 seconds. At its
+ * maximum tracked value (1 hour), it would still maintain a resolution of 3.6 seconds (or better).
+ * <p>
+ * Auto-resizing: When constructed with no specified value range range (or when auto-resize is turned on with {@link
+ * Histogram#setAutoResize}) a {@link PackedHistogram} will auto-resize its dynamic range to include recorded values as
+ * they are encountered. Note that recording calls that cause auto-resizing may take longer to execute, as resizing
+ * incurs allocation and copying of internal data structures.
+ * <p>
+ * See package description for {@link org.HdrHistogram} for details.
+ */
+
+public class PackedHistogram extends Histogram {
+
+    private PackedLongArray packedCounts;
+
+    @Override
+    long getCountAtIndex(final int index) {
+        return getCountAtNormalizedIndex(normalizeIndex(index, normalizingIndexOffset, countsArrayLength));
+    }
+
+    @Override
+    long getCountAtNormalizedIndex(final int index) {
+        long count = packedCounts.get(index);
+        return count;
+    }
+
+    @Override
+    void incrementCountAtIndex(final int index) {
+        packedCounts.increment(normalizeIndex(index, normalizingIndexOffset, countsArrayLength));
+    }
+
+    @Override
+    void addToCountAtIndex(final int index, final long value) {
+        packedCounts.add(normalizeIndex(index, normalizingIndexOffset, countsArrayLength), value);
+    }
+
+    @Override
+    void setCountAtIndex(int index, long value) {
+        setCountAtNormalizedIndex(normalizeIndex(index, normalizingIndexOffset, countsArrayLength), value);
+    }
+
+    @Override
+    void setCountAtNormalizedIndex(int index, long value) {
+        packedCounts.set(index, value);
+    }
+
+    @Override
+    void clearCounts() {
+        packedCounts.clear();
+        packedCounts.setVirtualLength(countsArrayLength);
+        totalCount = 0;
+    }
+
+    @Override
+    public PackedHistogram copy() {
+        PackedHistogram copy = new PackedHistogram(this);
+        copy.add(this);
+        return copy;
+    }
+
+    @Override
+    public PackedHistogram copyCorrectedForCoordinatedOmission(final long expectedIntervalBetweenValueSamples) {
+        PackedHistogram toHistogram = new PackedHistogram(this);
+        toHistogram.addWhileCorrectingForCoordinatedOmission(this, expectedIntervalBetweenValueSamples);
+        return toHistogram;
+    }
+
+    @Override
+    void resize(long newHighestTrackableValue) {
+        int oldNormalizedZeroIndex = normalizeIndex(0, normalizingIndexOffset, countsArrayLength);
+        int oldCountsArrayLength = countsArrayLength;
+
+        establishSize(newHighestTrackableValue);
+
+        if (oldNormalizedZeroIndex != 0) {
+            // We need to shift the stuff from the zero index and up to the end of the array:
+
+            // When things are shifted in a packed array its not simple to identify the region shifed,
+            // so re-record everything from the old normalized indexes to the new normalized indexes:
+
+            PackedLongArray newPackedCounts = new PackedLongArray(countsArrayLength, packedCounts.getPhysicalLength());
+            // Copy everything up to the oldNormalizedZeroIndex in place:
+            for (int fromIndex = 0; fromIndex < oldNormalizedZeroIndex; fromIndex++) {
+                long value = packedCounts.get(fromIndex);
+                if (value != 0) {
+                    newPackedCounts.set(fromIndex, value);
+                }
+            }
+            // Copy everything from the oldNormalizedZeroIndex to the end with an index delta shift:
+            int countsDelta = countsArrayLength - oldCountsArrayLength;
+
+            for (int fromIndex = oldNormalizedZeroIndex; fromIndex < oldCountsArrayLength; fromIndex++) {
+                long value = packedCounts.get(fromIndex);
+                if (value != 0) {
+                    int toIndex = fromIndex + countsDelta;
+                    newPackedCounts.set(toIndex, value);
+                }
+            }
+            // All unrecorded values are implicitly zero in the packed array
+
+            packedCounts = newPackedCounts;
+        } else {
+            packedCounts.setVirtualLength(countsArrayLength);
+        }
+    }
+
+    @Override
+    int _getEstimatedFootprintInBytes() {
+        return 192 + (8 * packedCounts.getPhysicalLength());
+    }
+
+    /**
+     * Construct an auto-resizing PackedHistogram with a lowest discernible value of 1 and an auto-adjusting
+     * highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedHistogram(final int numberOfSignificantValueDigits) {
+        this(1, 2, numberOfSignificantValueDigits);
+        setAutoResize(true);
+    }
+
+    /**
+     * Construct a PackedHistogram given the Highest value to be tracked and a number of significant decimal digits. The
+     * histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
+     *
+     * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
+     *                              integer that is {@literal >=} 2.
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedHistogram(final long highestTrackableValue, final int numberOfSignificantValueDigits) {
+        this(1, highestTrackableValue, numberOfSignificantValueDigits);
+    }
+
+    /**
+     * Construct a PackedHistogram given the Lowest and Highest values to be tracked and a number of significant
+     * decimal digits. Providing a lowestDiscernibleValue is useful is situations where the units used
+     * for the histogram's values are much smaller that the minimal accuracy required. E.g. when tracking
+     * time values stated in nanosecond units, where the minimal accuracy required is a microsecond, the
+     * proper value for lowestDiscernibleValue would be 1000.
+     *
+     * @param lowestDiscernibleValue The lowest value that can be tracked (distinguished from 0) by the histogram.
+     *                               Must be a positive integer that is {@literal >=} 1. May be internally rounded
+     *                               down to nearest power of 2.
+     * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
+     *                              integer that is {@literal >=} (2 * lowestDiscernibleValue).
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public PackedHistogram(final long lowestDiscernibleValue, final long highestTrackableValue, final int numberOfSignificantValueDigits) {
+        super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits, false);
+        packedCounts = new PackedLongArray(countsArrayLength);
+        wordSizeInBytes = 8;
+    }
+
+    /**
+     * Construct a PackedHistogram with the same range settings as a given source histogram,
+     * duplicating the source's start/end timestamps (but NOT it's contents)
+     * @param source The source histogram to duplicate
+     */
+    public PackedHistogram(final AbstractHistogram source) {
+        super(source, false);
+        packedCounts = new PackedLongArray(countsArrayLength);
+        wordSizeInBytes = 8;
+    }
+
+    /**
+     * Construct a new histogram by decoding it from a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
+     * @return The newly constructed histogram
+     */
+    public static PackedHistogram decodeFromByteBuffer(final ByteBuffer buffer,
+                                                       final long minBarForHighestTrackableValue) {
+        return (PackedHistogram) decodeFromByteBuffer(buffer, PackedHistogram.class,
+                minBarForHighestTrackableValue);
+    }
+
+    /**
+     * Construct a new histogram by decoding it from a compressed form in a ByteBuffer.
+     * @param buffer The buffer to decode from
+     * @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
+     * @return The newly constructed histogram
+     * @throws DataFormatException on error parsing/decompressing the buffer
+     */
+    public static PackedHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
+                                                                 final long minBarForHighestTrackableValue) throws DataFormatException {
+        return decodeFromCompressedByteBuffer(buffer, PackedHistogram.class, minBarForHighestTrackableValue);
+    }
+
+    private void readObject(final ObjectInputStream o)
+            throws IOException, ClassNotFoundException {
+        o.defaultReadObject();
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/Recorder.java b/src/main/java/org/HdrHistogram/Recorder.java
index 4a05e18..1125d7d 100644
--- a/src/main/java/org/HdrHistogram/Recorder.java
+++ b/src/main/java/org/HdrHistogram/Recorder.java
@@ -51,17 +51,39 @@ public class Recorder implements ValueRecorder {
     /**
      * Construct an auto-resizing {@link Recorder} with a lowest discernible value of
      * 1 and an auto-adjusting highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
+     * <p>
+     * Depending on the valuer of the <b><code>packed</code></b> parameter {@link Recorder} can be configuired to
+     * track value counts in a packed internal representation optimized for typical histogram recoded values are
+     * sparse in the value range and tend to be incremented in small unit counts. This packed representation tends
+     * to require significantly smaller amounts of stoarge when compared to unpacked representations, but can incur
+     * additional recording cost due to resizing and repacking operations that may
+     * occur as previously unrecorded values are encountered.
      *
      * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
      *                                       decimal digits to which the histogram will maintain value resolution
      *                                       and separation. Must be a non-negative integer between 0 and 5.
+     * @param packed Specifies whether the recorder will uses a packed internal representation or not.
      */
-    public Recorder(final int numberOfSignificantValueDigits) {
-        activeHistogram = new InternalConcurrentHistogram(instanceId, numberOfSignificantValueDigits);
+    public Recorder(final int numberOfSignificantValueDigits, boolean packed) {
+        activeHistogram = packed ?
+                new InternalPackedConcurrentHistogram(instanceId, numberOfSignificantValueDigits) :
+                new InternalConcurrentHistogram(instanceId, numberOfSignificantValueDigits);
         inactiveHistogram = null;
         activeHistogram.setStartTimeStamp(System.currentTimeMillis());
     }
 
+    /**
+     * Construct an auto-resizing {@link Recorder} with a lowest discernible value of
+     * 1 and an auto-adjusting highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public Recorder(final int numberOfSignificantValueDigits) {
+        this(numberOfSignificantValueDigits, false);
+    }
+
     /**
      * Construct a {@link Recorder} given the highest value to be tracked and a number of significant
      * decimal digits. The histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
@@ -165,7 +187,7 @@ public class Recorder implements ValueRecorder {
      * Get a new instance of an interval histogram, which will include a stable, consistent view of all value
      * counts accumulated since the last interval histogram was taken.
      * <p>
-     * Calling {@link Recorder#getIntervalHistogram()} will reset
+     * Calling {@code getIntervalHistogram()} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @return a histogram containing the value counts accumulated since the last interval histogram was taken.
@@ -178,24 +200,21 @@ public class Recorder implements ValueRecorder {
      * Get an interval histogram, which will include a stable, consistent view of all value counts
      * accumulated since the last interval histogram was taken.
      * <p>
-     * {@link Recorder#getIntervalHistogram(Histogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)}
+     * {@code getIntervalHistogram(histogramToRecycle)}
      * accepts a previously returned interval histogram that can be recycled internally to avoid allocation
      * and content copying operations, and is therefore significantly more efficient for repeated use than
      * {@link Recorder#getIntervalHistogram()} and
      * {@link Recorder#getIntervalHistogramInto getIntervalHistogramInto()}. The provided
      * {@code histogramToRecycle} must
      * be either be null or an interval histogram returned by a previous call to
-     * {@link Recorder#getIntervalHistogram(Histogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} or
+     * {@code getIntervalHistogram(histogramToRecycle)} or
      * {@link Recorder#getIntervalHistogram()}.
      * <p>
      * NOTE: The caller is responsible for not recycling the same returned interval histogram more than once. If
      * the same interval histogram instance is recycled more than once, behavior is undefined.
      * <p>
-     * Calling {@link Recorder#getIntervalHistogram(Histogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start accumulating value
-     * counts for the next interval
+     * Calling {@code getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start
+     * accumulating value counts for the next interval
      *
      * @param histogramToRecycle a previously returned interval histogram (from this instance of
      *                           {@link Recorder}) that may be recycled to avoid allocation and
@@ -251,7 +270,7 @@ public class Recorder implements ValueRecorder {
      * Place a copy of the value counts accumulated since accumulated (since the last interval histogram
      * was taken) into {@code targetHistogram}.
      *
-     * Calling {@link Recorder#getIntervalHistogramInto getIntervalHistogramInto()} will reset
+     * Calling {@code getIntervalHistogramInto(targetHistogram)} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @param targetHistogram the histogram into which the interval histogram's data should be copied
@@ -283,10 +302,16 @@ public class Recorder implements ValueRecorder {
                             activeHistogram.getLowestDiscernibleValue(),
                             activeHistogram.getHighestTrackableValue(),
                             activeHistogram.getNumberOfSignificantValueDigits());
-                } else {
+                } else if (activeHistogram instanceof InternalConcurrentHistogram) {
                     inactiveHistogram = new InternalConcurrentHistogram(
                             instanceId,
                             activeHistogram.getNumberOfSignificantValueDigits());
+                } else if (activeHistogram instanceof InternalPackedConcurrentHistogram) {
+                    inactiveHistogram = new InternalPackedConcurrentHistogram(
+                            instanceId,
+                            activeHistogram.getNumberOfSignificantValueDigits());
+                } else {
+                    throw new IllegalStateException("Unexpected internal histogram type for activeHistogram");
                 }
             }
 
@@ -311,7 +336,7 @@ public class Recorder implements ValueRecorder {
         }
     }
 
-    private class InternalAtomicHistogram extends AtomicHistogram {
+    private static class InternalAtomicHistogram extends AtomicHistogram {
         private final long containingInstanceId;
 
         private InternalAtomicHistogram(long id,
@@ -323,7 +348,7 @@ public class Recorder implements ValueRecorder {
         }
     }
 
-    private class InternalConcurrentHistogram extends ConcurrentHistogram {
+    private static class InternalConcurrentHistogram extends ConcurrentHistogram {
         private final long containingInstanceId;
 
         private InternalConcurrentHistogram(long id, int numberOfSignificantValueDigits) {
@@ -332,6 +357,15 @@ public class Recorder implements ValueRecorder {
         }
     }
 
+    private static class InternalPackedConcurrentHistogram extends PackedConcurrentHistogram {
+        private final long containingInstanceId;
+
+        private InternalPackedConcurrentHistogram(long id, int numberOfSignificantValueDigits) {
+            super(numberOfSignificantValueDigits);
+            this.containingInstanceId = id;
+        }
+    }
+
     private void validateFitAsReplacementHistogram(Histogram replacementHistogram,
                                                    boolean enforeContainingInstance) {
         boolean bad = true;
@@ -355,6 +389,15 @@ public class Recorder implements ValueRecorder {
                     )) {
                 bad = false;
             }
+        } else if (replacementHistogram instanceof InternalPackedConcurrentHistogram) {
+            if ((activeHistogram instanceof InternalPackedConcurrentHistogram)
+                    &&
+                    ((!enforeContainingInstance) ||
+                            (((InternalPackedConcurrentHistogram)replacementHistogram).containingInstanceId ==
+                                    ((InternalPackedConcurrentHistogram)activeHistogram).containingInstanceId)
+                    )) {
+                bad = false;
+            }
         }
         if (bad) {
             throw new IllegalArgumentException("replacement histogram must have been obtained via a previous" +
diff --git a/src/main/java/org/HdrHistogram/ShortCountsHistogram.java b/src/main/java/org/HdrHistogram/ShortCountsHistogram.java
index 33ac168..5d9d704 100644
--- a/src/main/java/org/HdrHistogram/ShortCountsHistogram.java
+++ b/src/main/java/org/HdrHistogram/ShortCountsHistogram.java
@@ -10,7 +10,6 @@ package org.HdrHistogram;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.nio.ByteBuffer;
-import java.nio.ShortBuffer;
 import java.util.Arrays;
 import java.util.zip.DataFormatException;
 
@@ -52,7 +51,7 @@ public class ShortCountsHistogram extends AbstractHistogram {
         long currentCount = counts[normalizedIndex];
         long newCount = (currentCount + value);
         if ((newCount < Short.MIN_VALUE) || (newCount > Short.MAX_VALUE)) {
-            throw new IllegalArgumentException("would overflow integer count");
+            throw new IllegalStateException("would overflow short integer count");
         }
         counts[normalizedIndex] = (short) newCount;
     }
@@ -65,7 +64,7 @@ public class ShortCountsHistogram extends AbstractHistogram {
     @Override
     void setCountAtNormalizedIndex(int index, long value) {
         if ((value < 0) || (value > Short.MAX_VALUE)) {
-            throw new IllegalArgumentException("would overflow short integer count");
+            throw new IllegalStateException("would overflow short integer count");
         }
         counts[index] = (short) value;
     }
@@ -169,8 +168,8 @@ public class ShortCountsHistogram extends AbstractHistogram {
     }
     
     /**
-     * Construct a ShortCountsHistogram given the Highest value to be tracked and a number of significant decimal digits. The
-     * histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
+     * Construct a ShortCountsHistogram given the Highest value to be tracked and a number of significant decimal
+     * digits. The histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
      *
      * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
      *                              integer that is {@literal >=} 2.
@@ -224,8 +223,7 @@ public class ShortCountsHistogram extends AbstractHistogram {
      */
     public static ShortCountsHistogram decodeFromByteBuffer(final ByteBuffer buffer,
                                                       final long minBarForHighestTrackableValue) {
-        return (ShortCountsHistogram) decodeFromByteBuffer(buffer, ShortCountsHistogram.class,
-                minBarForHighestTrackableValue);
+        return decodeFromByteBuffer(buffer, ShortCountsHistogram.class, minBarForHighestTrackableValue);
     }
 
     /**
@@ -236,17 +234,28 @@ public class ShortCountsHistogram extends AbstractHistogram {
      * @throws DataFormatException on error parsing/decompressing the buffer
      */
     public static ShortCountsHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
-                                                                final long minBarForHighestTrackableValue) throws DataFormatException {
+                                                                final long minBarForHighestTrackableValue)
+            throws DataFormatException {
         return decodeFromCompressedByteBuffer(buffer, ShortCountsHistogram.class, minBarForHighestTrackableValue);
     }
 
+    /**
+     * Construct a new ShortCountsHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A ShortCountsHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static ShortCountsHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
+    }
+
     private void readObject(final ObjectInputStream o)
             throws IOException, ClassNotFoundException {
         o.defaultReadObject();
     }
-
-    @Override
-    synchronized void fillCountsArrayFromBuffer(final ByteBuffer buffer, final int length) {
-        buffer.asShortBuffer().get(counts, 0, length);
-    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/HdrHistogram/SingleWriterDoubleRecorder.java b/src/main/java/org/HdrHistogram/SingleWriterDoubleRecorder.java
index cc5f0e4..33ab4fe 100644
--- a/src/main/java/org/HdrHistogram/SingleWriterDoubleRecorder.java
+++ b/src/main/java/org/HdrHistogram/SingleWriterDoubleRecorder.java
@@ -20,6 +20,8 @@ import java.util.concurrent.atomic.AtomicLong;
  * call {@link SingleWriterDoubleRecorder#recordValue} or
  * {@link SingleWriterDoubleRecorder#recordValueWithExpectedInterval} at any point in time.
  * It DOES NOT support concurrent recording calls.
+ * Recording calls are wait-free on architectures that support atomic increment operations, and
+ * are lock-free on architectures that do not.
  * <p>
  * A common pattern for using a {@link SingleWriterDoubleRecorder} looks like this:
  * <br><pre><code>
@@ -36,29 +38,51 @@ import java.util.concurrent.atomic.AtomicLong;
  * </code></pre>
  */
 
-public class SingleWriterDoubleRecorder {
+public class SingleWriterDoubleRecorder implements DoubleValueRecorder {
     private static AtomicLong instanceIdSequencer = new AtomicLong(1);
     private final long instanceId = instanceIdSequencer.getAndIncrement();
 
     private final WriterReaderPhaser recordingPhaser = new WriterReaderPhaser();
 
-    private volatile InternalDoubleHistogram activeHistogram;
-    private InternalDoubleHistogram inactiveHistogram;
+    private volatile DoubleHistogram activeHistogram;
+    private DoubleHistogram inactiveHistogram;
 
     /**
      * Construct an auto-resizing {@link SingleWriterDoubleRecorder} using a precision stated as a
      * number of significant decimal digits.
+     * <p>
+     * Depending on the valuer of the <b><code>packed</code></b> parameter {@link SingleWriterDoubleRecorder} can
+     * be configuired to track value counts in a packed internal representation optimized for typical histogram
+     * recoded values are sparse in the value range and tend to be incremented in small unit counts. This packed
+     * representation tends to require significantly smaller amounts of stoarge when compared to unpacked
+     * representations, but can incur additional recording cost due to resizing and repacking operations that may
+     * occur as previously unrecorded values are encountered.
      *
      * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
      *                                       decimal digits to which the histogram will maintain value resolution
      *                                       and separation. Must be a non-negative integer between 0 and 5.
+     * @param packed Specifies whether the recorder will uses a packed internal representation or not.
      */
-    public SingleWriterDoubleRecorder(final int numberOfSignificantValueDigits) {
-        activeHistogram = new InternalDoubleHistogram(instanceId, numberOfSignificantValueDigits);
+    public SingleWriterDoubleRecorder(final int numberOfSignificantValueDigits, final boolean packed) {
+        activeHistogram = packed ?
+                new PackedInternalDoubleHistogram(instanceId, numberOfSignificantValueDigits) :
+                new InternalDoubleHistogram(instanceId, numberOfSignificantValueDigits);
         inactiveHistogram = null;
         activeHistogram.setStartTimeStamp(System.currentTimeMillis());
     }
 
+    /**
+     * Construct an auto-resizing {@link SingleWriterDoubleRecorder} using a precision stated as a
+     * number of significant decimal digits.
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public SingleWriterDoubleRecorder(final int numberOfSignificantValueDigits) {
+        this(numberOfSignificantValueDigits, false);
+    }
+
     /**
      * Construct a {@link SingleWriterDoubleRecorder} dynamic range of values to cover and a number
      * of significant decimal digits.
@@ -136,7 +160,7 @@ public class SingleWriterDoubleRecorder {
      * Get a new instance of an interval histogram, which will include a stable, consistent view of all value
      * counts accumulated since the last interval histogram was taken.
      * <p>
-     * Calling {@link SingleWriterDoubleRecorder#getIntervalHistogram()} will reset
+     * Calling {@code getIntervalHistogram()} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @return a histogram containing the value counts accumulated since the last interval histogram was taken.
@@ -149,24 +173,21 @@ public class SingleWriterDoubleRecorder {
      * Get an interval histogram, which will include a stable, consistent view of all value counts
      * accumulated since the last interval histogram was taken.
      * <p>
-     * {@link SingleWriterDoubleRecorder#getIntervalHistogram(DoubleHistogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)}
+     * {@code getIntervalHistogram(histogramToRecycle)}
      * accepts a previously returned interval histogram that can be recycled internally to avoid allocation
      * and content copying operations, and is therefore significantly more efficient for repeated use than
      * {@link SingleWriterDoubleRecorder#getIntervalHistogram()} and
      * {@link SingleWriterDoubleRecorder#getIntervalHistogramInto getIntervalHistogramInto()}. The
      * provided {@code histogramToRecycle} must
      * be either be null or an interval histogram returned by a previous call to
-     * {@link SingleWriterDoubleRecorder#getIntervalHistogram(DoubleHistogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} or
+     * {@code getIntervalHistogram(histogramToRecycle)} or
      * {@link SingleWriterDoubleRecorder#getIntervalHistogram()}.
      * <p>
      * NOTE: The caller is responsible for not recycling the same returned interval histogram more than once. If
      * the same interval histogram instance is recycled more than once, behavior is undefined.
      * <p>
      * Calling
-     * {@link SingleWriterDoubleRecorder#getIntervalHistogram(DoubleHistogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start accumulating value
+     * {@code getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start accumulating value
      * counts for the next interval
      *
      * @param histogramToRecycle a previously returned interval histogram (from this instance of
@@ -213,7 +234,7 @@ public class SingleWriterDoubleRecorder {
                                                              boolean enforeContainingInstance) {
         // Verify that replacement histogram can validly be used as an inactive histogram replacement:
         validateFitAsReplacementHistogram(histogramToRecycle, enforeContainingInstance);
-        inactiveHistogram = (InternalDoubleHistogram) histogramToRecycle;
+        inactiveHistogram = histogramToRecycle;
         performIntervalSample();
         DoubleHistogram sampledHistogram = inactiveHistogram;
         inactiveHistogram = null; // Once we expose the sample, we can't reuse it internally until it is recycled
@@ -224,7 +245,7 @@ public class SingleWriterDoubleRecorder {
      * Place a copy of the value counts accumulated since accumulated (since the last interval histogram
      * was taken) into {@code targetHistogram}.
      *
-     * Calling {@link SingleWriterDoubleRecorder#getIntervalHistogramInto}() will
+     * Calling {@code getIntervalHistogramInto(targetHistogram)} will
      * reset the value counts, and start accumulating value counts for the next interval.
      *
      * @param targetHistogram the histogram into which the interval histogram's data should be copied
@@ -249,13 +270,20 @@ public class SingleWriterDoubleRecorder {
 
             // Make sure we have an inactive version to flip in:
             if (inactiveHistogram == null) {
-                inactiveHistogram = new InternalDoubleHistogram(activeHistogram);
+                if (activeHistogram instanceof InternalDoubleHistogram) {
+                    inactiveHistogram = new InternalDoubleHistogram((InternalDoubleHistogram) activeHistogram);
+                } else if (activeHistogram instanceof PackedInternalDoubleHistogram) {
+                    inactiveHistogram = new PackedInternalDoubleHistogram(
+                            instanceId, activeHistogram.getNumberOfSignificantValueDigits());
+                } else {
+                    throw new IllegalStateException("Unexpected internal histogram type for activeHistogram");
+                }
             }
 
             inactiveHistogram.reset();
 
             // Swap active and inactive histograms:
-            final InternalDoubleHistogram tempHistogram = inactiveHistogram;
+            final DoubleHistogram tempHistogram = inactiveHistogram;
             inactiveHistogram = activeHistogram;
             activeHistogram = tempHistogram;
 
@@ -294,6 +322,15 @@ public class SingleWriterDoubleRecorder {
         }
     }
 
+    private class PackedInternalDoubleHistogram extends PackedDoubleHistogram {
+        private final long containingInstanceId;
+
+        private PackedInternalDoubleHistogram(long id, int numberOfSignificantValueDigits) {
+            super(numberOfSignificantValueDigits);
+            this.containingInstanceId = id;
+        }
+    }
+
     private void validateFitAsReplacementHistogram(DoubleHistogram replacementHistogram,
                                                    boolean enforeContainingInstance) {
         boolean bad = true;
@@ -303,7 +340,14 @@ public class SingleWriterDoubleRecorder {
                 &&
                 ((!enforeContainingInstance) ||
                         (((InternalDoubleHistogram) replacementHistogram).containingInstanceId ==
-                        activeHistogram.containingInstanceId)
+                                ((InternalDoubleHistogram)activeHistogram).containingInstanceId)
+                )) {
+            bad = false;
+        } else if ((replacementHistogram instanceof PackedInternalDoubleHistogram)
+                &&
+                ((!enforeContainingInstance) ||
+                        (((PackedInternalDoubleHistogram) replacementHistogram).containingInstanceId ==
+                                ((PackedInternalDoubleHistogram)activeHistogram).containingInstanceId)
                 )) {
             bad = false;
         }
diff --git a/src/main/java/org/HdrHistogram/SingleWriterRecorder.java b/src/main/java/org/HdrHistogram/SingleWriterRecorder.java
index 1cdc2bb..00cec31 100644
--- a/src/main/java/org/HdrHistogram/SingleWriterRecorder.java
+++ b/src/main/java/org/HdrHistogram/SingleWriterRecorder.java
@@ -21,6 +21,8 @@ import java.util.concurrent.atomic.AtomicLong;
  * call {@link SingleWriterRecorder#recordValue} or
  * {@link SingleWriterRecorder#recordValueWithExpectedInterval} at any point in time.
  * It DOES NOT safely support concurrent recording calls.
+ * Recording calls are wait-free on architectures that support atomic increment operations, and
+ * re lock-free on architectures that do not.
  *  * <p>
  * A common pattern for using a {@link SingleWriterRecorder} looks like this:
  * <br><pre><code>
@@ -43,23 +45,45 @@ public class SingleWriterRecorder implements ValueRecorder {
 
     private final WriterReaderPhaser recordingPhaser = new WriterReaderPhaser();
 
-    private volatile InternalHistogram activeHistogram;
-    private InternalHistogram inactiveHistogram;
+    private volatile Histogram activeHistogram;
+    private Histogram inactiveHistogram;
 
     /**
      * Construct an auto-resizing {@link SingleWriterRecorder} with a lowest discernible value of
      * 1 and an auto-adjusting highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
+     * <p>
+     * Depending on the valuer of the <b><code>packed</code></b> parameter {@link SingleWriterRecorder} can be configuired to
+     * track value counts in a packed internal representation optimized for typical histogram recoded values are
+     * sparse in the value range and tend to be incremented in small unit counts. This packed representation tends
+     * to require significantly smaller amounts of stoarge when compared to unpacked representations, but can incur
+     * additional recording cost due to resizing and repacking operations that may
+     * occur as previously unrecorded values are encountered.
      *
      * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
      *                                       decimal digits to which the histogram will maintain value resolution
      *                                       and separation. Must be a non-negative integer between 0 and 5.
+     * @param packed Specifies whether the recorder will uses a packed internal representation or not.
      */
-    public SingleWriterRecorder(final int numberOfSignificantValueDigits) {
-        activeHistogram = new InternalHistogram(instanceId, numberOfSignificantValueDigits);
+    public SingleWriterRecorder(final int numberOfSignificantValueDigits, final boolean packed) {
+        activeHistogram = packed ?
+                new PackedInternalHistogram(instanceId, numberOfSignificantValueDigits) :
+                new InternalHistogram(instanceId, numberOfSignificantValueDigits);
         inactiveHistogram = null;
         activeHistogram.setStartTimeStamp(System.currentTimeMillis());
     }
 
+    /**
+     * Construct an auto-resizing {@link SingleWriterRecorder} with a lowest discernible value of
+     * 1 and an auto-adjusting highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
+     *
+     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
+     *                                       decimal digits to which the histogram will maintain value resolution
+     *                                       and separation. Must be a non-negative integer between 0 and 5.
+     */
+    public SingleWriterRecorder(final int numberOfSignificantValueDigits) {
+        this(numberOfSignificantValueDigits, false);
+    }
+
     /**
      * Construct a {@link SingleWriterRecorder} given the highest value to be tracked and a number
      * of significant decimal digits. The histogram will be constructed to implicitly track (distinguish from 0)
@@ -164,7 +188,7 @@ public class SingleWriterRecorder implements ValueRecorder {
      * Get a new instance of an interval histogram, which will include a stable, consistent view of all value
      * counts accumulated since the last interval histogram was taken.
      * <p>
-     * Calling {@link SingleWriterRecorder#getIntervalHistogram()} will reset
+     * Calling {@code getIntervalHistogram()} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @return a histogram containing the value counts accumulated since the last interval histogram was taken.
@@ -177,24 +201,21 @@ public class SingleWriterRecorder implements ValueRecorder {
      * Get an interval histogram, which will include a stable, consistent view of all value counts
      * accumulated since the last interval histogram was taken.
      * <p>
-     * {@link SingleWriterRecorder#getIntervalHistogram(Histogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)}
+     * {@code getIntervalHistogram(histogramToRecycle)}
      * accepts a previously returned interval histogram that can be recycled internally to avoid allocation
      * and content copying operations, and is therefore significantly more efficient for repeated use than
      * {@link SingleWriterRecorder#getIntervalHistogram()} and
      * {@link SingleWriterRecorder#getIntervalHistogramInto getIntervalHistogramInto()}. The provided
      * {@code histogramToRecycle} must
      * be either be null or an interval histogram returned by a previous call to
-     * {@link SingleWriterRecorder#getIntervalHistogram(Histogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} or
+     * {@code getIntervalHistogram(histogramToRecycle)} or
      * {@link SingleWriterRecorder#getIntervalHistogram()}.
      * <p>
      * NOTE: The caller is responsible for not recycling the same returned interval histogram more than once. If
      * the same interval histogram instance is recycled more than once, behavior is undefined.
      * <p>
-     * Calling {@link SingleWriterRecorder#getIntervalHistogram(Histogram histogramToRecycle)
-     * getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start accumulating value
-     * counts for the next interval
+     * Calling {@code getIntervalHistogram(histogramToRecycle)} will reset the value counts, and start
+     * accumulating value counts for the next interval
      *
      * @param histogramToRecycle a previously returned interval histogram (from this instance of
      *                           {@link SingleWriterRecorder}) that may be recycled to avoid allocation and
@@ -239,7 +260,7 @@ public class SingleWriterRecorder implements ValueRecorder {
                                                        boolean enforeContainingInstance) {
         // Verify that replacement histogram can validly be used as an inactive histogram replacement:
         validateFitAsReplacementHistogram(histogramToRecycle, enforeContainingInstance);
-        inactiveHistogram = (InternalHistogram) histogramToRecycle;
+        inactiveHistogram = histogramToRecycle;
         performIntervalSample();
         Histogram sampledHistogram = inactiveHistogram;
         inactiveHistogram = null; // Once we expose the sample, we can't reuse it internally until it is recycled
@@ -250,7 +271,7 @@ public class SingleWriterRecorder implements ValueRecorder {
      * Place a copy of the value counts accumulated since accumulated (since the last interval histogram
      * was taken) into {@code targetHistogram}.
      *
-     * Calling {@link SingleWriterRecorder#getIntervalHistogramInto getIntervalHistogramInto()} will reset
+     * Calling {@code getIntervalHistogramInto(targetHistogram)} will reset
      * the value counts, and start accumulating value counts for the next interval.
      *
      * @param targetHistogram the histogram into which the interval histogram's data should be copied
@@ -276,13 +297,20 @@ public class SingleWriterRecorder implements ValueRecorder {
 
             // Make sure we have an inactive version to flip in:
             if (inactiveHistogram == null) {
-                inactiveHistogram = new InternalHistogram(activeHistogram);
+                if (activeHistogram instanceof InternalHistogram) {
+                    inactiveHistogram = new InternalHistogram((InternalHistogram) activeHistogram);
+                } else if (activeHistogram instanceof PackedInternalHistogram) {
+                    inactiveHistogram = new PackedInternalHistogram(
+                            instanceId, activeHistogram.getNumberOfSignificantValueDigits());
+                } else {
+                    throw new IllegalStateException("Unexpected internal histogram type for activeHistogram");
+                }
             }
 
             inactiveHistogram.reset();
 
             // Swap active and inactive histograms:
-            final InternalHistogram tempHistogram = inactiveHistogram;
+            final Histogram tempHistogram = inactiveHistogram;
             inactiveHistogram = activeHistogram;
             activeHistogram = tempHistogram;
 
@@ -300,7 +328,7 @@ public class SingleWriterRecorder implements ValueRecorder {
         }
     }
 
-    private class InternalHistogram extends Histogram {
+    private static class InternalHistogram extends Histogram {
         private final long containingInstanceId;
 
         private InternalHistogram(long id, int numberOfSignificantValueDigits) {
@@ -322,6 +350,15 @@ public class SingleWriterRecorder implements ValueRecorder {
         }
     }
 
+    private static class PackedInternalHistogram extends PackedHistogram {
+        private final long containingInstanceId;
+
+        private PackedInternalHistogram(long id, int numberOfSignificantValueDigits) {
+            super(numberOfSignificantValueDigits);
+            this.containingInstanceId = id;
+        }
+    }
+
     private void validateFitAsReplacementHistogram(Histogram replacementHistogram,
                                                    boolean enforeContainingInstance) {
         boolean bad = true;
@@ -331,7 +368,14 @@ public class SingleWriterRecorder implements ValueRecorder {
                 &&
                 ((!enforeContainingInstance) ||
                         (((InternalHistogram) replacementHistogram).containingInstanceId ==
-                                activeHistogram.containingInstanceId)
+                                ((InternalHistogram) activeHistogram).containingInstanceId)
+                )) {
+            bad = false;
+        } else if ((replacementHistogram instanceof PackedInternalHistogram)
+                &&
+                ((!enforeContainingInstance) ||
+                        (((PackedInternalHistogram) replacementHistogram).containingInstanceId ==
+                                ((PackedInternalHistogram) activeHistogram).containingInstanceId)
                 )) {
             bad = false;
         }
diff --git a/src/main/java/org/HdrHistogram/SynchronizedDoubleHistogram.java b/src/main/java/org/HdrHistogram/SynchronizedDoubleHistogram.java
index 0a19d5c..2fc01dc 100644
--- a/src/main/java/org/HdrHistogram/SynchronizedDoubleHistogram.java
+++ b/src/main/java/org/HdrHistogram/SynchronizedDoubleHistogram.java
@@ -7,14 +7,8 @@
 
 package org.HdrHistogram;
 
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.PrintStream;
 import java.nio.ByteBuffer;
-import java.util.Iterator;
-import java.util.zip.DataFormatException;
-import java.util.zip.Deflater;
 
 /**
  * <h3>A floating point values High Dynamic Range (HDR) Histogram that is synchronized as a whole</h3>
diff --git a/src/main/java/org/HdrHistogram/SynchronizedHistogram.java b/src/main/java/org/HdrHistogram/SynchronizedHistogram.java
index b2b50ef..e8b6428 100644
--- a/src/main/java/org/HdrHistogram/SynchronizedHistogram.java
+++ b/src/main/java/org/HdrHistogram/SynchronizedHistogram.java
@@ -9,15 +9,9 @@ package org.HdrHistogram;
 
 import java.io.IOException;
 import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.PrintStream;
 import java.nio.ByteBuffer;
-import java.nio.LongBuffer;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Locale;
 import java.util.zip.DataFormatException;
-import java.util.zip.Deflater;
 
 /**
  * <h3>An integer values High Dynamic Range (HDR) Histogram that is synchronized as a whole</h3>
@@ -117,6 +111,21 @@ public class SynchronizedHistogram extends Histogram {
         return decodeFromCompressedByteBuffer(buffer, SynchronizedHistogram.class, minBarForHighestTrackableValue);
     }
 
+    /**
+     * Construct a new SynchronizedHistogram by decoding it from a String containing a base64 encoded
+     * compressed histogram representation.
+     *
+     * @param base64CompressedHistogramString A string containing a base64 encoding of a compressed histogram
+     * @return A SynchronizedHistogram decoded from the string
+     * @throws DataFormatException on error parsing/decompressing the input
+     */
+    public static SynchronizedHistogram fromString(final String base64CompressedHistogramString)
+            throws DataFormatException {
+        return decodeFromCompressedByteBuffer(
+                ByteBuffer.wrap(Base64Helper.parseBase64Binary(base64CompressedHistogramString)),
+                0);
+    }
+
     @Override
     public synchronized long getTotalCount() {
         return super.getTotalCount();
@@ -151,6 +160,7 @@ public class SynchronizedHistogram extends Histogram {
     /**
      * @deprecated
      */
+    @SuppressWarnings("deprecation")
     @Override
     public synchronized void recordValue(final long value, final long expectedIntervalBetweenValueSamples)
             throws ArrayIndexOutOfBoundsException {
@@ -178,11 +188,13 @@ public class SynchronizedHistogram extends Histogram {
     }
 
 
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     @Override
     public void copyInto(final AbstractHistogram targetHistogram) {
         // Synchronize copyInto(). Avoid deadlocks by synchronizing in order of construction identity count.
         if (identity < targetHistogram.identity) {
             synchronized (this) {
+                //noinspection SynchronizationOnLocalVariableOrMethodParameter
                 synchronized (targetHistogram) {
                     super.copyInto(targetHistogram);
                 }
@@ -196,6 +208,7 @@ public class SynchronizedHistogram extends Histogram {
         }
     }
 
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     @Override
     public void copyIntoCorrectedForCoordinatedOmission(final AbstractHistogram targetHistogram,
                                                         final long expectedIntervalBetweenValueSamples) {
@@ -216,6 +229,7 @@ public class SynchronizedHistogram extends Histogram {
         }
     }
 
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     @Override
     public void add(final AbstractHistogram otherHistogram) {
         // Synchronize add(). Avoid deadlocks by synchronizing in order of construction identity count.
@@ -234,6 +248,7 @@ public class SynchronizedHistogram extends Histogram {
         }
     }
 
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     @Override
     public void subtract(final AbstractHistogram otherHistogram)
             throws ArrayIndexOutOfBoundsException, IllegalArgumentException {
@@ -253,6 +268,7 @@ public class SynchronizedHistogram extends Histogram {
         }
     }
 
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     @Override
     public void addWhileCorrectingForCoordinatedOmission(final AbstractHistogram fromHistogram,
                                                          final long expectedIntervalBetweenValueSamples) {
@@ -282,6 +298,7 @@ public class SynchronizedHistogram extends Histogram {
         super.shiftValuesRight(numberOfBinaryOrdersOfMagnitude);
     }
 
+    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
     @Override
     public boolean equals(final Object other){
         if ( this == other ) {
diff --git a/src/main/java/org/HdrHistogram/WriterReaderPhaser.java b/src/main/java/org/HdrHistogram/WriterReaderPhaser.java
index accd2ed..34f6fab 100644
--- a/src/main/java/org/HdrHistogram/WriterReaderPhaser.java
+++ b/src/main/java/org/HdrHistogram/WriterReaderPhaser.java
@@ -10,39 +10,129 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater;
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
- * {@link WriterReaderPhaser} instances provide an asymmetric means for synchronizing the execution of
- * wait-free "writer" critical sections against a "reader phase flip" that needs to make sure no writer critical
- * sections that were active at the beginning of the flip are still active after the flip is done. Multiple writers
- * and multiple readers are supported.
+ * {@link WriterReaderPhaser} provides an asymmetric means for
+ * synchronizing the execution of wait-free "writer" critical sections against
+ * a "reader phase flip" that needs to make sure no writer critical sections
+ * that were active at the beginning of the flip are still active after the
+ * flip is done.  Multiple writers and multiple readers are supported.
  * <p>
- * While a {@link WriterReaderPhaser} can be useful in multiple scenarios, a specific and common use case is
- * that of safely managing "double buffered" data stream access in which writers can proceed without being
- * blocked, while readers gain access to stable and unchanging buffer samples
- * <blockquote>
- * NOTE: {@link WriterReaderPhaser} writers are wait-free on architectures that support wait-free atomic
- * increment operations. They remain lock-free (but not wait-free) on architectures that do not support
- * wait-free atomic increment operations.
- * </blockquote>
- * {@link WriterReaderPhaser} "writers" are wait free, "readers" block for other "readers", and
- * "readers" are only blocked by "writers" whose critical section was entered before the reader's
+ * Using a {@link WriterReaderPhaser} for coordination, writers can continously
+ * perform wait-free/lock-free updates to common data structures, while readers
+ * can get hold of atomic and inactive snapshots without stalling writers.
+ * <p>
+ * While a {@link WriterReaderPhaser} can be useful in multiple scenarios, a
+ * specific and common use case is that of safely managing "double buffered"
+ * data stream access in which writers can proceed without being blocked, while
+ * readers gain access to stable and unchanging buffer samples.
+ * {@link WriterReaderPhaser} "writers" are wait free (on architectures that support
+ * wait free atomic increment operations), "readers" block for other
+ * "readers", and "readers" are only blocked by "writers" whose critical section
+ * was entered before the reader's
  * {@link WriterReaderPhaser#flipPhase()} attempt.
+ * <h3>Assumptions and Guarantees</h3>
  * <p>
- * When used to protect an actively recording data structure, the assumptions on how readers and writers act are:
+ * When used to protect an actively recording data structure, the assumptions on
+ * how readers and writers act are:
  * <ol>
  * <li>There are two sets of data structures ("active" and "inactive")</li>
- * <li>Writing is done to the perceived active version (as perceived by the writer), and only
- *     within critical sections delineated by {@link WriterReaderPhaser#writerCriticalSectionEnter}
- *     and {@link WriterReaderPhaser#writerCriticalSectionExit}).</li>
- * <li>Only readers switch the perceived roles of the active and inactive data structures.
- *     They do so only while under readerLock(), and only before calling flipPhase().</li>
+ * <li>Writing is done to the perceived active version (as perceived by the
+ *     writer), and only within critical sections delineated by
+ *     {@link WriterReaderPhaser#writerCriticalSectionEnter} and
+ *     {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionExit()}.
+ *     </li>
+ * <li>Only readers switch the perceived roles of the active and inactive data
+ *     structures. They do so only while under {@link WriterReaderPhaser#readerLock()}
+ *     protection and only before calling {@link WriterReaderPhaser#flipPhase()}.</li>
+ * <li>Writers do not remain in their critical sections indefinitely.</li>
+ * <li>Only writers perform {@link WriterReaderPhaser#writerCriticalSectionEnter}
+ *     and
+ *     {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionExit()}.
+ * </li>
+ * <li>Readers do not hold onto readerLock indefinitely.</li>
+ * <li>Only readers perform {@link WriterReaderPhaser#readerLock()} and
+ * {@link WriterReaderPhaser#readerUnlock()}.</li>
+ * <li>Only readers perform {@link WriterReaderPhaser#flipPhase()} operations,
+ * and only while holding the readerLock.</li>
+ * </ol>
+ * <p>
+ * When the above assumptions are met, {@link WriterReaderPhaser} guarantees
+ * that the inactive data structures are not being modified by any writers while
+ * being read while under readerLock() protection after a
+ * {@link WriterReaderPhaser#flipPhase()}() operation.
+ * <p>
+ * The following progress guarantees are provided to writers and readers that
+ * adhere to the above stated assumptions:
+ * <ol>
+ * <li>Writers operations
+ * ({@link WriterReaderPhaser#writerCriticalSectionEnter writerCriticalSectionEnter}
+ * and {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionExit})
+ * are wait free on architectures that
+ * support wait-free atomic increment operations (they remain lock-free [but not
+ * wait-free] on architectures that do not support wait-free atomic increment
+ * operations)</li>
+ * <li>{@link WriterReaderPhaser#flipPhase()} operations are guaranteed to
+ * make forward progress, and will only be blocked by writers whose critical sections
+ * were entered prior to the start of the reader's flipPhase operation, and have not
+ * yet exited their critical sections.</li>
+ * <li>{@link WriterReaderPhaser#readerLock()} only blocks for other
+ * readers that are holding the readerLock.</li>
  * </ol>
- * When the above assumptions are met, {@link WriterReaderPhaser} guarantees that the inactive data structures are not
- * being modified by any writers while being read while under readerLock() protection after a flipPhase()
- * operation.
  *
+ * <h3>Example use</h3>
+ * Imagine a simple use case where a histogram (which is basically a large set of
+ * rapidly updated counters) is being modified by writers, and a reader needs to gain
+ * access to stable interval samples of the histogram for reporting or other analysis
+ * purposes.
+ * <pre><code>
+ *         final WriterReaderPhaser recordingPhaser = new WriterReaderPhaser();
+ *
+ *         volatile Histogram activeHistogram;
+ *         Histogram inactiveHistogram;
+ *         ...
+ * </code></pre>
+ * A writer may record values the histogram:
+ * <pre><code>
+ *         // Wait-free recording:
+ *         long criticalValueAtEnter = recordingPhaser.writerCriticalSectionEnter();
+ *         try {
+ *             activeHistogram.recordValue(value);
+ *         } finally {
+ *             recordingPhaser.writerCriticalSectionExit(criticalValueAtEnter);
+ *         }
+ * </code></pre>
+ * A reader gains access to a stable histogram of values recorded during an interval,
+ * and reports on it:
+ * <pre><code>
+ *         try {
+ *             recordingPhaser.readerLock();
  *
+ *             inactiveHistogram.reset();
  *
+ *             // Swap active and inactive histograms:
+ *             final Histogram tempHistogram = inactiveHistogram;
+ *             inactiveHistogram = activeHistogram;
+ *             activeHistogram = tempHistogram;
+ *
+ *             recordingPhaser.flipPhase();
+ *             // At this point, inactiveHistogram content is guaranteed to be stable
+ *
+ *             logHistogram(inactiveHistogram);
+ *
+ *         } finally {
+ *             recordingPhaser.readerUnlock();
+ *         }
+ * </code></pre>
  */
+/*
+ * High level design: There are even and odd epochs; the epoch flips for each
+ * reader.  Any number of writers can be in the same epoch (odd or even), but
+ * after a completed phase flip no writers will be still in the old epoch
+ * (and therefor are known to not be updating or observing the old, inactive
+ * data structure). Writers can always proceed at full speed in what they
+ * percieve to be the current (odd or even) epoch. The epoch flip is fast (a
+ * single atomic op).
+ */
+
 public class WriterReaderPhaser {
     private volatile long startEpoch = 0;
     private volatile long evenEndEpoch = 0;
@@ -63,7 +153,7 @@ public class WriterReaderPhaser {
      * This call is wait-free on architectures that support wait free atomic increment operations,
      * and is lock-free on architectures that do not.
      * <p>
-     * {@link WriterReaderPhaser#writerCriticalSectionEnter()} must be matched with a subsequent
+     * {@code writerCriticalSectionEnter()} must be matched with a subsequent
      * {@link WriterReaderPhaser#writerCriticalSectionExit(long)} in order for CriticalSectionPhaser
      * synchronization to function properly.
      *
@@ -80,7 +170,7 @@ public class WriterReaderPhaser {
      * This call is wait-free on architectures that support wait free atomic increment operations,
      * and is lock-free on architectures that do not.
      * <p>
-     * {@link WriterReaderPhaser#writerCriticalSectionExit(long)} must be matched with a preceding
+     * {@code writerCriticalSectionExit(long)} must be matched with a preceding
      * {@link WriterReaderPhaser#writerCriticalSectionEnter()} call, and must be provided with the
      * matching {@link WriterReaderPhaser#writerCriticalSectionEnter()} call's return value, in
      * order for CriticalSectionPhaser synchronization to function properly.
@@ -89,16 +179,12 @@ public class WriterReaderPhaser {
      * {@link WriterReaderPhaser#writerCriticalSectionEnter()} call.
      */
     public void writerCriticalSectionExit(long criticalValueAtEnter) {
-        if (criticalValueAtEnter < 0) {
-            oddEndEpochUpdater.getAndIncrement(this);
-        } else {
-            evenEndEpochUpdater.getAndIncrement(this);
-        }
+        (criticalValueAtEnter < 0 ? oddEndEpochUpdater : evenEndEpochUpdater).getAndIncrement(this);
     }
 
     /**
-     * Enter to a critical section containing a read operation (mutually excludes against other
-     * {@link WriterReaderPhaser#readerLock} calls).
+     * Enter to a critical section containing a read operation (reentrant, mutually excludes against
+     * {@link WriterReaderPhaser#readerLock} calls by other threads).
      * <p>
      * {@link WriterReaderPhaser#readerLock} DOES NOT provide synchronization
      * against {@link WriterReaderPhaser#writerCriticalSectionEnter()} calls. Use {@link WriterReaderPhaser#flipPhase()}
@@ -118,19 +204,19 @@ public class WriterReaderPhaser {
 
     /**
      * Flip a phase in the {@link WriterReaderPhaser} instance, {@link WriterReaderPhaser#flipPhase()}
-     * can only be called while holding the readerLock().
+     * can only be called while holding the {@link WriterReaderPhaser#readerLock() readerLock}.
      * {@link WriterReaderPhaser#flipPhase()} will return only after all writer critical sections (protected by
-     * {@link WriterReaderPhaser#writerCriticalSectionEnter()} ()} and
-     * {@link WriterReaderPhaser#writerCriticalSectionExit(long)} ()}) that may have been in flight when the
-     * {@link WriterReaderPhaser#flipPhase()} call were made had completed.
+     * {@link WriterReaderPhaser#writerCriticalSectionEnter() writerCriticalSectionEnter} and
+     * {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionEnter}) that may have been
+     * in flight when the {@link WriterReaderPhaser#flipPhase()} call were made had completed.
      * <p>
      * No actual writer critical section activity is required for {@link WriterReaderPhaser#flipPhase()} to
      * succeed.
      * <p>
      * However, {@link WriterReaderPhaser#flipPhase()} is lock-free with respect to calls to
      * {@link WriterReaderPhaser#writerCriticalSectionEnter()} and
-     * {@link WriterReaderPhaser#writerCriticalSectionExit(long)}. It may spin-wait for for active
-     * writer critical section code to complete.
+     * {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionExit()}. It may spin-wait
+     * or for active writer critical section code to complete.
      *
      * @param yieldTimeNsec The amount of time (in nanoseconds) to sleep in each yield if yield loop is needed.
      */
@@ -139,58 +225,45 @@ public class WriterReaderPhaser {
             throw new IllegalStateException("flipPhase() can only be called while holding the readerLock()");
         }
 
+        // Read the volatile 'startEpoch' exactly once
         boolean nextPhaseIsEven = (startEpoch < 0); // Current phase is odd...
 
-        long initialStartValue;
         // First, clear currently unused [next] phase end epoch (to proper initial value for phase):
-        if (nextPhaseIsEven) {
-            initialStartValue = 0;
-            evenEndEpochUpdater.lazySet(this, initialStartValue);
-        } else {
-            initialStartValue = Long.MIN_VALUE;
-            oddEndEpochUpdater.lazySet(this, initialStartValue);
-        }
+        long initialStartValue = nextPhaseIsEven ? 0 : Long.MIN_VALUE;
+        (nextPhaseIsEven ? evenEndEpochUpdater : oddEndEpochUpdater).lazySet(this, initialStartValue);
 
         // Next, reset start value, indicating new phase, and retain value at flip:
         long startValueAtFlip = startEpochUpdater.getAndSet(this, initialStartValue);
 
         // Now, spin until previous phase end value catches up with start value at flip:
-        boolean caughtUp = false;
-        do {
-            if (nextPhaseIsEven) {
-                caughtUp = (oddEndEpoch == startValueAtFlip);
+        while((nextPhaseIsEven ? oddEndEpoch : evenEndEpoch) != startValueAtFlip)  {
+            if (yieldTimeNsec == 0) {
+                Thread.yield();
             } else {
-                caughtUp = (evenEndEpoch == startValueAtFlip);
-            }
-            if (!caughtUp) {
-                if (yieldTimeNsec == 0) {
-                    Thread.yield();
-                } else {
-                    try {
-                        TimeUnit.NANOSECONDS.sleep(yieldTimeNsec);
-                    } catch (InterruptedException ex) {
-                    }
+                try {
+                    TimeUnit.NANOSECONDS.sleep(yieldTimeNsec);
+                } catch (InterruptedException ex) {
+                    // nothing to do here, we just woke up earlier that expected.
                 }
             }
-        } while (!caughtUp);
+        }
     }
 
-
     /**
-     * Flip a phase in the {@link WriterReaderPhaser} instance, {@link WriterReaderPhaser#flipPhase()}
-     * can only be called while holding the readerLock().
-     * {@link WriterReaderPhaser#flipPhase()} will return only after all writer critical sections (protected by
-     * {@link WriterReaderPhaser#writerCriticalSectionEnter()} ()} and
-     * {@link WriterReaderPhaser#writerCriticalSectionExit(long)} ()}) that may have been in flight when the
-     * {@link WriterReaderPhaser#flipPhase()} call were made had completed.
+     * Flip a phase in the {@link WriterReaderPhaser} instance, {@code flipPhase()}
+     * can only be called while holding the {@link WriterReaderPhaser#readerLock() readerLock}.
+     * {@code flipPhase()} will return only after all writer critical sections (protected by
+     * {@link WriterReaderPhaser#writerCriticalSectionEnter() writerCriticalSectionEnter} and
+     * {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionEnter}) that may have been
+     * in flight when the {@code flipPhase()} call were made had completed.
      * <p>
-     * No actual writer critical section activity is required for {@link WriterReaderPhaser#flipPhase()} to
+     * No actual writer critical section activity is required for {@code flipPhase()} to
      * succeed.
      * <p>
-     * However, {@link WriterReaderPhaser#flipPhase()} is lock-free with respect to calls to
+     * However, {@code flipPhase()} is lock-free with respect to calls to
      * {@link WriterReaderPhaser#writerCriticalSectionEnter()} and
-     * {@link WriterReaderPhaser#writerCriticalSectionExit(long)}. It may spin-wait for for active
-     * writer critical section code to complete.
+     * {@link WriterReaderPhaser#writerCriticalSectionExit writerCriticalSectionExit()}. It may spin-wait
+     * or for active writer critical section code to complete.
      */
     public void flipPhase() {
         flipPhase(0);
diff --git a/src/main/java/org/HdrHistogram/packedarray/AbstractPackedArrayContext.java b/src/main/java/org/HdrHistogram/packedarray/AbstractPackedArrayContext.java
new file mode 100644
index 0000000..9517d55
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/AbstractPackedArrayContext.java
@@ -0,0 +1,1093 @@
+package org.HdrHistogram.packedarray;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * A packed-value, sparse array context used for storing 64 bit signed values.
+ *
+ * An array context is optimised for tracking sparsely set (as in mostly zeros) values that tend to not make
+ * use pof the full 64 bit value range even when they are non-zero. The array context's internal representation
+ * is such that the packed value at each virtual array index may be represented by 0-8 bytes of actual storage.
+ *
+ * An array context encodes the packed values in 8 "set trees" with each set tree representing one byte of the
+ * packed value at the virtual index in question. The {@link #getPackedIndex(int, int, boolean)} method is used
+ * to look up the byte-index corresponding to the given (set tree) value byte of the given virtual index, and can
+ * be used to add entries to represent that byte as needed. As a succesful {@link #getPackedIndex(int, int, boolean)}
+ * may require a resizing of the array, it can throw a {@link ResizeException} to indicate that the requested
+ * packed index cannot be found or added without a resize of the physical storage.
+ *
+ */
+abstract class AbstractPackedArrayContext implements Serializable {
+    /**
+     *
+     * The physical representation uses an insert-at-the-end mechanism for adding contents to the array. Any
+     * insertion will occur at the very end of the array, and any expansion of an element will move it to the end,
+     * leaving an empty slot behind.
+     *
+     * Terminology:
+     *
+     * long-word: a 64-bit-aligned 64 bit word
+     * short-word: a 16-bit-aligned 16 bit word
+     * byte: an 8-bit-aligned byte
+     *
+     * long-index: an index of a 64-bit-aligned word within the overall array (i.e. in multiples of 8 bytes)
+     * short-index: an index of a 16-bit aligned short within the overall array (i.e. in multiples of 2 bytes)
+     * byte-index: an index of an 8-bit aligned byte within the overall array (i.e. in multiples of 1 byte)
+     *
+     * The storage array stores long (64 bit) words. Lookups for the variuous sizes are done as such:
+     *
+     * long getAtLongIndex(int longIndex) { return array[longIndex]; }
+     * short getAtShortIndex(int shortIndex) { return (short)((array[shortIndex >> 2] >> (shortIndex & 0x3)) & 0xffff); }
+     * byte getAtByteIndex(int byteIndex) { return (byte)((array[byteIndex >> 3] >> (byteIndex & 0x7)) & 0xff); }
+     *
+     * [Therefore there is no dependence on byte endiannes of the underlying arhcitecture]
+     *
+     * Structure:
+     *
+     * The packed array captures values at virtual indexes in a collection of striped "set trees" (also called "sets"),
+     * with each set tree representing one byte of the value at the virtual index in question. As such, there are 8
+     * sets in the array, each corresponding to a byte in the overall value being stored. Set 0 contains the LSByte
+     * of the value, and Set 7 contains the MSByte of the value.
+     *
+     * The array contents is comprised of thre types of entries:
+     *  - The root indexes: A fixed size 8 short-words array of short indexes at the start of the array, containing
+     *    the short-index of the root entry of each of the 8 set trees.
+     *
+     *  - Non-Leaf Entires: Variable sized, 2-18 short-words entries representing non-leaf entries in a set tree.
+     *    Non-Leaf entries comprise of a 2 short-word header containing a packed slot indicators bitmask and the
+     *    (optional non-zero) index of previous version of the entry, followed by an array of 0-16 shortwords.
+     *    The short-word found at a given slot in this array holds an index to an entry in the next level of
+     *    the set tree.
+     *
+     *  - Leaf Entries: comprised of long-words. Each byte [0-7] in the longword holds an actual value. Specifically,
+     *    the byte-index of that LeafEntry byte in the array is the byte-index for the given set's byte value of a
+     *    virtual index.
+     *
+     * If a given virtual index for a given set has no entry in a given set tree, the byte value for that set of
+     * that virtual index interpreted as 0. If a given set tree does not have an entry for a given virtual index,
+     * it is safe to assume that no higher significance set tree have one either.
+     **
+     * Non-leaf entries structure and mutation protocols:
+     *
+     * The structure of a Non-Leaf entry in the array can be roughly desctibed in terms of this C-stylre struct:
+     *
+     * struct nonLeafEntry {
+     *     short packedSlotIndicators;
+     *     short previousVersionIndex;
+     *     short[] enrtrySlotsIndexes;
+     * }
+     *
+     * Non-leaf entries are 2-18 short-words in length, with the length determined by the number of bits set in
+     * the packedSlotIndicators short-word in the entry. The packed slot indicators short-word is a bit mask which
+     * represents the 16 possible next-level entries below the given entry, and has a bit set (to '1') for each slot
+     * that is actually populated with a next level entry. Each of the short-words in the enrtrySlots is
+     * associated with a specific active ('1') bit in the packedSlotIndicators short-word, and holds the index
+     * to the next level's entry associated with ta given path in the tree. [Note: the values in enrtrySlotsIndexes[]
+     * are short-indexes if the next level is not a leaf level, and long-indexes if the next level is
+     * a leaf.]
+     *
+     * Summary of Non-leaf entry use and replacement protocol:
+     *
+     * - No value in any enrtrySlotsIndexes[] array is ever initialized to a zero value. Zero values in
+     *   enrtrySlotsIndexes[] can only appear through consolidation (see below). Once an enrtrySlotsIndexes[]
+     *   slot is observed to contain a zero, it cannot change to a non-zero value.
+     *
+     * - Zero values encountered in enrtrySlotsIndexes[] arrays are never followed. If a zero value is found
+     *   when looking for the index to a lower level entry during a tree walk, the tree walking operation is
+     *   restarted from the root.
+     *
+     * - A Non-Leaf entry with an active (non zero index) previous version is never followed or expanded.
+     *   Instead, any thread encountering a Non-leaf entry with an active previous version will consolidate
+     *   the previous version with the current one. the consolidation opeartion will clear (zero) the
+     *   previousVersionIndex, which will then allow the caller to continue with whatever use the thread was
+     *   attempting to make of the entry.
+     *
+     * - Expansion of entries: Since entries hold only enough storage to represent currently populated paths
+     *   below them in the set tree, any addition of entries at a lower level requires the expansion of the entry
+     *   to make room for a larger enrtrySlotsIndexes array. Expansion allocates a new and larger entry structure,
+     *   and populates the newly inserted slot in it with an index to a newly allocated next-level entry. It then
+     *   links the newly expanded entry the previous entry structure via the previousVersionIndex field, and
+     *   publishes the newly expanded entry by [atomically] replacing the "pointer index" to the previous entry
+     *   (located at a higher level entry's slot, or in the root indexes) with a "pointer index" to the newly
+     *   expanded entry structure.  A failure to atomically publish a newly expanded entry (e.g. if the "pointer
+     *   index" being replaced holds a value other than that in our not-yet-published previousVersionIndex) will
+     *   restart the expansion operation from the beginning.
+     *   When first published, a newly-visible expanded entry is immediately "usable" because it has an active,
+     *   "not yet consolidated" previous version entry, and any user of the entry will first have to consolidate it.
+     *   The expansion will follow publication of the expanded entry with a consolidation of the previous entry
+     *   into the new one, clearing the previousVersionIndex field in the process, and enabling normal use of
+     *   the expanded entry.
+     *
+     * - Concurrent consolidation: While expansion and consolidation are ongoing, other threads can be
+     *   concurrently walking the set trees. Per the protocol stated here, any tree walk encountering a Non-Leaf
+     *   entry with an active previous version will consolidate the entry before using it. Consolidation can
+     *   of a given entry can occur concurrently by an an expanding thread and by multiple walking threads.
+     *
+     * - Consolidation of a a previous version entry into a current one is done by:
+     *      - For each non-zero index in the previous version enrty, copy that index to the new assocaited
+     *        entry slot in the entry, and CAS a zero in the old entry slot. If the CAS fails, repeat (including
+     *        the zero check).
+     *      - Once all entry slots in the previous version entry have been consolidated and zeroed, zero
+     *        the index to the previous version entry.
+     */
+
+    private static final int PACKED_ARRAY_GROWTH_INCREMENT = 16;
+    private static final int PACKED_ARRAY_GROWTH_FRACTION_POW2 = 4;
+    private static final int SET_0_START_INDEX = 0;
+    private static final int NUMBER_OF_SETS = 8;
+    private static final int LEAF_LEVEL_SHIFT = 3;
+    private static final int NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS = 2;
+    private static final int NON_LEAF_ENTRY_SLOT_INDICATORS_OFFSET = 0;
+    private static final int NON_LEAF_ENTRY_PREVIOUS_VERSION_OFFSET = 1;
+
+    static final int MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY = 16;
+    static final int MAX_SUPPORTED_PACKED_COUNTS_ARRAY_LENGTH = (Short.MAX_VALUE / 4);
+
+    private final boolean isPacked;
+    private int physicalLength;
+    private int virtualLength = 0;
+    private int topLevelShift = Integer.MAX_VALUE; // Make it non-sensical until properly initialized.
+
+    AbstractPackedArrayContext(final int virtualLength, final int initialPhysicalLength) {
+        physicalLength = Math.max(initialPhysicalLength, MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY);
+        isPacked = (physicalLength <= AbstractPackedArrayContext.MAX_SUPPORTED_PACKED_COUNTS_ARRAY_LENGTH);
+        if (!isPacked) {
+            physicalLength = virtualLength;
+        }
+    }
+
+    void init(final int virtualLength) {
+        if (!isPacked()) {
+            // Deal with non-packed context init:
+            this.virtualLength = virtualLength;
+            return;
+        }
+        // room for the 8 shorts root indexes:
+        do {
+        } while (!casPopulatedShortLength(getPopulatedShortLength(), SET_0_START_INDEX + 8));
+
+        // Populate empty root entries, and point to them from the root indexes:
+        for (int i = 0; i < NUMBER_OF_SETS; i++) {
+            setAtShortIndex(SET_0_START_INDEX + i, (short) 0);
+        }
+        setVirtualLength(virtualLength);
+    }
+
+    //
+    //     ###    ########   ######  ######## ########     ###     ######  ########  ######
+    //    ## ##   ##     ## ##    ##    ##    ##     ##   ## ##   ##    ##    ##    ##    ##
+    //   ##   ##  ##     ## ##          ##    ##     ##  ##   ##  ##          ##    ##
+    //  ##     ## ########   ######     ##    ########  ##     ## ##          ##     ######
+    //  ######### ##     ##       ##    ##    ##   ##   ######### ##          ##          ##
+    //  ##     ## ##     ## ##    ##    ##    ##    ##  ##     ## ##    ##    ##    ##    ##
+    //  ##     ## ########   ######     ##    ##     ## ##     ##  ######     ##     ######
+    //
+
+
+    abstract int length();
+
+    abstract int getPopulatedShortLength();
+
+    abstract boolean casPopulatedShortLength(int expectedPopulatedShortLength, int newPopulatedShortLength);
+
+    abstract boolean casPopulatedLongLength(int expectedPopulatedShortLength, int newPopulatedShortLength);
+
+    abstract long getAtLongIndex(int longIndex);
+
+    abstract boolean casAtLongIndex(int longIndex, long expectedValue, long newValue);
+
+    abstract void lazySetAtLongIndex(int longIndex, long newValue);
+
+    abstract void clearContents();
+
+    abstract void resizeArray(int newLength);
+
+    abstract long getAtUnpackedIndex(int index);
+
+    abstract void setAtUnpackedIndex(int index, long newValue);
+
+    abstract void lazysetAtUnpackedIndex(int index, long newValue);
+
+    abstract long incrementAndGetAtUnpackedIndex(int index);
+
+    abstract long addAndGetAtUnpackedIndex(int index, long valueToAdd);
+
+    abstract String unpackedToString();
+
+    //
+    //  ########  ########  #### ##     ## #### ######## #### ##     ## ########     #######  ########   ######
+    //  ##     ## ##     ##  ##  ###   ###  ##     ##     ##  ##     ## ##          ##     ## ##     ## ##    ##
+    //  ##     ## ##     ##  ##  #### ####  ##     ##     ##  ##     ## ##          ##     ## ##     ## ##
+    //  ########  ########   ##  ## ### ##  ##     ##     ##  ##     ## ######      ##     ## ########   ######
+    //  ##        ##   ##    ##  ##     ##  ##     ##     ##   ##   ##  ##          ##     ## ##              ##
+    //  ##        ##    ##   ##  ##     ##  ##     ##     ##    ## ##   ##          ##     ## ##        ##    ##
+    //  ##        ##     ## #### ##     ## ####    ##    ####    ###    ########     #######  ##         ######
+    //
+
+    void setValuePart(final int longIndex,
+                      final long valuePartAsLong,
+                      final long valuePartMask,
+                      final int valuePartShift) {
+        boolean success;
+        do {
+            long currentLongValue = getAtLongIndex(longIndex);
+            long newLongValue = (currentLongValue & ~valuePartMask) | (valuePartAsLong << valuePartShift);
+            success = casAtLongIndex(longIndex, currentLongValue, newLongValue);
+        }
+        while (!success);
+    }
+
+    short getAtShortIndex(final int shortIndex) {
+        return (short) ((getAtLongIndex(shortIndex >> 2) >> ((shortIndex & 0x3) << 4)) & 0xffff);
+    }
+
+    short getIndexAtShortIndex(final int shortIndex) {
+        return (short) ((getAtLongIndex(shortIndex >> 2) >> ((shortIndex & 0x3) << 4)) & 0x7fff);
+    }
+
+    void setAtShortIndex(final int shortIndex, final short value) {
+        int longIndex = shortIndex >> 2;
+        int shortShift = (shortIndex & 0x3) << 4;
+        long shortMask = ((long) 0xffff) << shortShift;
+        long shortValueAsLong = ((long) value) & 0xffff;
+        setValuePart(longIndex, shortValueAsLong, shortMask, shortShift);
+    }
+
+    boolean casAtShortIndex(final int shortIndex, final short expectedValue, final short newValue) {
+        int longIndex = shortIndex >> 2;
+        int shortShift = (shortIndex & 0x3) << 4;
+        long shortMask = ~(((long) 0xffff) << shortShift);
+        long newShortValueAsLong = ((long) newValue) & 0xffff;
+        long expectedShortValueAsLong = ((long) expectedValue) & 0xffff;
+        boolean success;
+        do {
+            long currentLongValue = getAtLongIndex(longIndex);
+            long currentShortValueAsLong = (currentLongValue >> shortShift) & 0xffff;
+            if (currentShortValueAsLong != expectedShortValueAsLong) {
+                return false;
+            }
+            long newLongValue = (currentLongValue & shortMask) | (newShortValueAsLong << shortShift);
+            success = casAtLongIndex(longIndex, currentLongValue, newLongValue);
+        }
+        while (!success);
+        return true;
+    }
+
+    byte getAtByteIndex(final int byteIndex) {
+        return (byte) ((getAtLongIndex(byteIndex >> 3) >> ((byteIndex & 0x7) << 3)) & 0xff);
+    }
+
+    void setAtByteIndex(final int byteIndex, final byte value) {
+        int longIndex = byteIndex >> 3;
+        int byteShift = (byteIndex & 0x7) << 3;
+        long byteMask = ((long) 0xff) << byteShift;
+        long byteValueAsLong = ((long) value) & 0xff;
+        setValuePart(longIndex, byteValueAsLong, byteMask, byteShift);
+    }
+
+    /**
+     * add a byte value to a current byte value in the array
+     * @param byteIndex index of byte value to add to
+     * @param valueToAdd byte value to add
+     * @return the afterAddValue. ((afterAddValue & 0x100) != 0) indicates a carry.
+     */
+    long addAtByteIndex(final int byteIndex, final byte valueToAdd) {
+        int longIndex = byteIndex >> 3;
+        int byteShift = (byteIndex & 0x7) << 3;
+        long byteMask = ((long) 0xff) << byteShift;
+        boolean success;
+        long newValue;
+        do {
+            long currentLongValue = getAtLongIndex(longIndex);
+            long byteValueAsLong = (currentLongValue >> byteShift) & 0xff;
+            newValue = byteValueAsLong + (((long) valueToAdd) & 0xff);
+            long newByteValueAsLong = newValue & 0xff;
+            long newLongValue = (currentLongValue & ~byteMask) | (newByteValueAsLong << byteShift);
+            success = casAtLongIndex(longIndex, currentLongValue, newLongValue);
+        }
+        while (!success);
+        return newValue;
+    }
+
+    //
+    //  ######## ##    ## ######## ########  ##    ##    ######## #### ######## ##       ########   ######
+    //  ##       ###   ##    ##    ##     ##  ##  ##     ##        ##  ##       ##       ##     ## ##    ##
+    //  ##       ####  ##    ##    ##     ##   ####      ##        ##  ##       ##       ##     ## ##
+    //  ######   ## ## ##    ##    ########     ##       ######    ##  ######   ##       ##     ##  ######
+    //  ##       ##  ####    ##    ##   ##      ##       ##        ##  ##       ##       ##     ##       ##
+    //  ##       ##   ###    ##    ##    ##     ##       ##        ##  ##       ##       ##     ## ##    ##
+    //  ######## ##    ##    ##    ##     ##    ##       ##       #### ######## ######## ########   ######
+    //
+
+    private int getPackedSlotIndicators(final int entryIndex) {
+        return ((int) getAtShortIndex(entryIndex + NON_LEAF_ENTRY_SLOT_INDICATORS_OFFSET)) & 0xffff;
+    }
+
+    private void setPackedSlotIndicators(final int entryIndex, final short newPackedSlotIndicators) {
+        setAtShortIndex(entryIndex + NON_LEAF_ENTRY_SLOT_INDICATORS_OFFSET, newPackedSlotIndicators);
+    }
+
+    private short getPreviousVersionIndex(final int entryIndex) {
+        return getAtShortIndex(entryIndex + NON_LEAF_ENTRY_PREVIOUS_VERSION_OFFSET);
+    }
+
+    private void setPreviousVersionIndex(final int entryIndex, final short newPreviosVersionIndex) {
+        setAtShortIndex(entryIndex + NON_LEAF_ENTRY_PREVIOUS_VERSION_OFFSET, newPreviosVersionIndex);
+    }
+
+    private short getIndexAtEntrySlot(final int entryIndex, final int slot) {
+        return getAtShortIndex(entryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + slot);
+    }
+
+    private void setIndexAtEntrySlot(final int entryIndex, final int slot, final short newIndexValue) {
+        setAtShortIndex(entryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + slot, newIndexValue);
+    }
+
+    private boolean casIndexAtEntrySlot(final int entryIndex,
+                                        final int slot,
+                                        final short expectedIndexValue,
+                                        final short newIndexValue) {
+        return casAtShortIndex(entryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + slot,
+                expectedIndexValue, newIndexValue);
+    }
+
+    private boolean casIndexAtEntrySlotIfNonZeroAndLessThan(final int entryIndex,
+                                                            final int slot,
+                                                            final short newIndexValue) {
+        boolean success;
+        do {
+            short existingIndexValue = getIndexAtEntrySlot(entryIndex, slot);
+            if (existingIndexValue == 0) return false;
+            if (newIndexValue <= existingIndexValue) return false;
+            success = casIndexAtEntrySlot(entryIndex, slot, existingIndexValue, newIndexValue);
+        } while (!success);
+        return true;
+    }
+
+    //
+    //  ######## ##    ## ######## ########  ##    ##     #######  ########   ######
+    //  ##       ###   ##    ##    ##     ##  ##  ##     ##     ## ##     ## ##    ##
+    //  ##       ####  ##    ##    ##     ##   ####      ##     ## ##     ## ##
+    //  ######   ## ## ##    ##    ########     ##       ##     ## ########   ######
+    //  ##       ##  ####    ##    ##   ##      ##       ##     ## ##              ##
+    //  ##       ##   ###    ##    ##    ##     ##       ##     ## ##        ##    ##
+    //  ######## ##    ##    ##    ##     ##    ##        #######  ##         ######
+    //
+
+    private void expandArrayIfNeeded(final int entryLengthInLongs) throws ResizeException {
+        final int currentLength = length();
+        if (length() < getPopulatedLongLength() + entryLengthInLongs) {
+            int growthIncrement = Math.max(entryLengthInLongs, PACKED_ARRAY_GROWTH_INCREMENT);
+            growthIncrement = Math.max(growthIncrement, getPopulatedLongLength() >> PACKED_ARRAY_GROWTH_FRACTION_POW2);
+            throw new ResizeException(currentLength + growthIncrement);
+        }
+    }
+
+    private int newEntry(final int entryLengthInShorts) throws ResizeException {
+        // Add entry at the end of the array:
+        int newEntryIndex;
+        boolean success;
+        do {
+            newEntryIndex = getPopulatedShortLength();
+            expandArrayIfNeeded((entryLengthInShorts >> 2) + 1);
+            success = casPopulatedShortLength(newEntryIndex, (newEntryIndex + entryLengthInShorts));
+        } while (!success);
+
+        for (int i = 0; i < entryLengthInShorts; i++) {
+            setAtShortIndex(newEntryIndex + i, (short) -1); // Poison value -1. Must be overriden before reads
+        }
+        return newEntryIndex;
+    }
+
+    private int newLeafEntry() throws ResizeException {
+        // Add entry at the end of the array:
+        int newEntryIndex;
+        boolean success;
+        do {
+            newEntryIndex = getPopulatedLongLength();
+            expandArrayIfNeeded(1);
+            success = casPopulatedLongLength(newEntryIndex, (newEntryIndex + 1));
+        } while (!success);
+
+        lazySetAtLongIndex(newEntryIndex, 0);
+        return newEntryIndex;
+    }
+
+    /**
+     * Consolidate entry with previous entry verison if one exists
+     *
+     * @param entryIndex The shortIndex of the entry to be consolidated
+     */
+    private void consolidateEntry(final int entryIndex) {
+        int previousVersionIndex = getPreviousVersionIndex(entryIndex);
+        if (previousVersionIndex == 0) return;
+        if (getPreviousVersionIndex(previousVersionIndex) != 0) {
+            throw new IllegalStateException("Encountered Previous Version Entry that is not itself consolidated.");
+        }
+
+        int previousVersionPackedSlotsIndicators = getPackedSlotIndicators(previousVersionIndex);
+        // Previous version exists, needs consolidation
+
+        int packedSlotsIndicators = getPackedSlotIndicators(entryIndex);
+        int insertedSlotMask = packedSlotsIndicators ^ previousVersionPackedSlotsIndicators; // the only bit that differs
+        int slotsBelowBitNumber = packedSlotsIndicators & (insertedSlotMask - 1);
+        int insertedSlotIndex = Integer.bitCount(slotsBelowBitNumber);
+        int numberOfSlotsInEntry = Integer.bitCount(packedSlotsIndicators);
+
+        // Copy the entry slots from previous version, skipping the newly inserted slot in the target:
+        int sourceSlot = 0;
+        for (int targetSlot = 0; targetSlot < numberOfSlotsInEntry; targetSlot++) {
+            if (targetSlot != insertedSlotIndex) {
+                boolean success = true;
+                do {
+                    short indexAtSlot = getIndexAtEntrySlot(previousVersionIndex, sourceSlot);
+                    if (indexAtSlot != 0) {
+                        // Copy observed index at slot to current entry
+                        // (only copy value in if previous value is less than new one AND is non-zero)
+                        casIndexAtEntrySlotIfNonZeroAndLessThan(entryIndex, targetSlot, indexAtSlot);
+
+                        // CAS the previous verison slot to 0.
+                        // (Succeeds only if the index in that slot has not changed. Retry if it did).
+                        success = casIndexAtEntrySlot(previousVersionIndex, sourceSlot, indexAtSlot, (short) 0);
+                    }
+                }
+                while (!success);
+                sourceSlot++;
+            }
+        }
+
+        setPreviousVersionIndex(entryIndex, (short) 0);
+    }
+
+    /**
+     * Expand entry as indicated.
+     *
+     * @param existingEntryIndex the index of the entry
+     * @param entryPointerIndex  index to the slot pointing to the entry (needs to be fixed up)
+     * @param insertedSlotIndex  realtive [packed] index of slot being inserted into entry
+     * @param insertedSlotMask   mask value fo slot being inserted
+     * @param nextLevelIsLeaf    the level below this one is a leaf level
+     * @return the updated index of the entry (-1 if epansion failed due to conflict)
+     * @throws RetryException if expansion fails due to concurrent conflict, and caller should try again.
+     */
+    private int expandEntry(final int existingEntryIndex,
+                            final int entryPointerIndex,
+                            final int insertedSlotIndex,
+                            final int insertedSlotMask,
+                            final boolean nextLevelIsLeaf)throws RetryException, ResizeException {
+        int packedSlotIndicators = ((int) getAtShortIndex(existingEntryIndex)) & 0xffff;
+        packedSlotIndicators |= insertedSlotMask;
+        int numberOfslotsInExpandedEntry = Integer.bitCount(packedSlotIndicators);
+        if (insertedSlotIndex >= numberOfslotsInExpandedEntry) {
+            throw new IllegalStateException("inserted slot index is out of range given provided masks");
+        }
+        int expandedEntryLength = numberOfslotsInExpandedEntry + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS;
+
+        // Create new next-level entry to refer to from slot at this level:
+        int indexOfNewNextLevelEntry = 0;
+        if (nextLevelIsLeaf) {
+            indexOfNewNextLevelEntry = newLeafEntry(); // Establish long-index to new leaf entry
+        } else {
+            // TODO: Optimize this by creating the whole sub-tree here, rather than a step that will immediaterly expand
+            // Create a new 1 word (empty, no slots set) entry for the next level:
+            indexOfNewNextLevelEntry = newEntry(NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS); // Establish short-index to new leaf entry
+            setPackedSlotIndicators(indexOfNewNextLevelEntry, (short) 0);
+            setPreviousVersionIndex(indexOfNewNextLevelEntry, (short) 0);
+        }
+        short insertedSlotValue = (short) indexOfNewNextLevelEntry;
+
+        int expandedEntryIndex = newEntry(expandedEntryLength);
+
+        // populate the packed indicators word:
+        setPackedSlotIndicators(expandedEntryIndex, (short) packedSlotIndicators);
+        setPreviousVersionIndex(expandedEntryIndex, (short) existingEntryIndex);
+
+        // Populate the inserted slot with the iundex of the new next level entry:
+        setIndexAtEntrySlot(expandedEntryIndex, insertedSlotIndex, insertedSlotValue);
+
+        // Copy of previous version entries is deferred to later consolidateEntry() call.
+
+        // Set the pointer to the updated entry index. If CAS fails, discard by throwing retry expecption.
+        boolean success = casAtShortIndex(entryPointerIndex, (short) existingEntryIndex, (short) expandedEntryIndex);
+        if (!success) {
+            throw new RetryException();
+        }
+
+        // Exanded entry is published, now consolidate it:
+
+        consolidateEntry(expandedEntryIndex);
+
+        return expandedEntryIndex;
+
+    }
+
+
+
+    //
+    //   ######   ######## ########    ##     ##    ###    ##             ## #### ##    ## ########  ######## ##     ##
+    //  ##    ##  ##          ##       ##     ##   ## ##   ##            ##   ##  ###   ## ##     ## ##        ##   ##
+    //  ##        ##          ##       ##     ##  ##   ##  ##           ##    ##  ####  ## ##     ## ##         ## ##
+    //  ##   #### ######      ##       ##     ## ##     ## ##          ##     ##  ## ## ## ##     ## ######      ###
+    //  ##    ##  ##          ##        ##   ##  ######### ##         ##      ##  ##  #### ##     ## ##         ## ##
+    //  ##    ##  ##          ##         ## ##   ##     ## ##        ##       ##  ##   ### ##     ## ##        ##   ##
+    //   ######   ########    ##          ###    ##     ## ######## ##       #### ##    ## ########  ######## ##     ##
+    //
+
+
+    private int getRootEntry(final int setNumber) {
+        try {
+            return getRootEntry(setNumber, false);
+        } catch (RetryException | ResizeException ex) {
+            throw new IllegalStateException("Should not Resize or Retry exceptions on real-only read: ", ex);
+        }
+
+    }
+
+    private int getRootEntry(final int setNumber, boolean insertAsNeeded) throws RetryException, ResizeException {
+        int entryPointerIndex = SET_0_START_INDEX + setNumber;
+        int entryIndex = getIndexAtShortIndex(entryPointerIndex);
+
+        if (entryIndex == 0) {
+            if (!insertAsNeeded) {
+                return 0; // Index does not currently exist in packed array;
+            }
+
+            entryIndex = newEntry(NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS);
+            // Create a new empty (no slots set) entry for the next level:
+            setPackedSlotIndicators(entryIndex, (short) 0);
+            setPreviousVersionIndex(entryIndex, (short) 0);
+
+            boolean success = casAtShortIndex(entryPointerIndex, (short) 0, (short) entryIndex);
+            if (!success) {
+                throw new RetryException();
+            }
+        }
+
+        if (((getTopLevelShift() != LEAF_LEVEL_SHIFT)) && getPreviousVersionIndex(entryIndex) != 0) {
+            consolidateEntry(entryIndex);
+        }
+        return entryIndex;
+    }
+
+    /**
+     * Get the byte-index (into the packed array) corresponding to a given (set tree) value byte of given virtual index.
+     * Inserts new set tree nodes as needed if indicated.
+     *
+     * @param setNumber      The set tree number (0-7, 0 corresponding with the LSByte set tree)
+     * @param virtualIndex   The virtual index into the PackedArray
+     * @param insertAsNeeded If true, will insert new set tree nodes as needed if they do not already exist
+     * @return the byte-index corresponding to the given (set tree) value byte of the given virtual index
+     */
+    int getPackedIndex(final int setNumber, final int virtualIndex, final boolean insertAsNeeded)
+            throws ResizeException {
+        int byteIndex = 0; // Must be overwritten to finish. Will retry until non-zero.
+        do {
+            try {
+                assert (setNumber >= 0 && setNumber < NUMBER_OF_SETS);
+                if (virtualIndex >= getVirtualLength()) {
+                    throw new ArrayIndexOutOfBoundsException(
+                            String.format("Attempting access at index %d, beyond virtualLength %d",
+                                    virtualIndex, getVirtualLength()));
+                }
+                int entryPointerIndex = SET_0_START_INDEX + setNumber;
+                int entryIndex = getRootEntry(setNumber, insertAsNeeded);
+                if (entryIndex == 0) {
+                    return -1; // Index does not currently exist in packed array;
+                }
+
+                // Work down the levels of non-leaf entries:
+                for (int indexShift = getTopLevelShift(); indexShift >= LEAF_LEVEL_SHIFT; indexShift -= 4) {
+                    boolean nextLevelIsLeaf = (indexShift == LEAF_LEVEL_SHIFT);
+                    // Target is a packedSlotIndicators entry
+                    int packedSlotIndicators = getPackedSlotIndicators(entryIndex);
+                    int slotBitNumber = (virtualIndex >>> indexShift) & 0xf;
+                    int slotMask = 1 << slotBitNumber;
+                    int slotsBelowBitNumber = packedSlotIndicators & (slotMask - 1);
+                    int slotNumber = Integer.bitCount(slotsBelowBitNumber);
+
+                    if ((packedSlotIndicators & slotMask) == 0) {
+                        // The entryIndex slot does not have the contents we want
+                        if (!insertAsNeeded) {
+                            return -1; // Index does not currently exist in packed array;
+                        }
+
+                        // Expand the entry, adding the index to new entry at the proper slot:
+                        entryIndex = expandEntry(entryIndex, entryPointerIndex, slotNumber, slotMask, nextLevelIsLeaf);
+                    }
+
+                    // Next level's entry pointer index is in the appropriate slot in in the entries array in this entry:
+                    entryPointerIndex = entryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + slotNumber;
+
+                    entryIndex = getIndexAtShortIndex(entryPointerIndex);
+                    if (entryIndex == 0) {
+                        throw new RetryException();
+                    }
+                    if ((!nextLevelIsLeaf) && getPreviousVersionIndex(entryIndex) != 0) {
+                        consolidateEntry(entryIndex);
+                    }
+
+                    // entryIndex is either holds the long-index of a leaf entry, or the shorty-index of the next
+                    // level entry's packed slot indicators short-word.
+                }
+
+                // entryIndex is the long-index of a leaf entry that contains the value byte for the given set
+
+                byteIndex = (entryIndex << 3) + (virtualIndex & 0x7); // Determine byte index offset within leaf entry
+
+            } catch (RetryException ignored) {
+                // Retry will happen automatically since byteIndex was not set to non-zero value;
+            }
+        }
+        while (byteIndex == 0);
+
+        return byteIndex;
+    }
+
+    private long contextLocalGetValueAtIndex(final int virtualIndex) {
+        long value = 0;
+        for (int byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) {
+            int packedIndex = 0;
+            long byteValueAtPackedIndex;
+            do {
+                try {
+                    packedIndex = getPackedIndex(byteNum, virtualIndex, false);
+                    if (packedIndex < 0) {
+                        return value;
+                    }
+                    byteValueAtPackedIndex = (((long) getAtByteIndex(packedIndex)) & 0xff) << (byteNum << 3);
+                } catch (ResizeException ex) {
+                    throw new IllegalStateException("Should never encounter a resize excpetion without inserts");
+                }
+            } while (packedIndex == 0);
+
+            value += byteValueAtPackedIndex;
+        }
+        return value;
+    }
+
+    //
+    //  ##     ##         ########   #######  ########  ##     ## ##          ###    ######## ########
+    //   ##   ##          ##     ## ##     ## ##     ## ##     ## ##         ## ##      ##    ##
+    //    ## ##           ##     ## ##     ## ##     ## ##     ## ##        ##   ##     ##    ##
+    //     ###    ####### ########  ##     ## ########  ##     ## ##       ##     ##    ##    ######
+    //    ## ##           ##        ##     ## ##        ##     ## ##       #########    ##    ##
+    //   ##   ##          ##        ##     ## ##        ##     ## ##       ##     ##    ##    ##
+    //  ##     ##         ##         #######  ##         #######  ######## ##     ##    ##    ########
+    //
+
+
+    void populateEquivalentEntriesWithZerosFromOther(final AbstractPackedArrayContext other) {
+        if (getVirtualLength() < other.getVirtualLength()) {
+            throw new IllegalStateException("Cannot populate array of smaller virtrual length");
+        }
+        for (int i = 0; i < NUMBER_OF_SETS; i++) {
+            int otherEntryIndex = other.getAtShortIndex(SET_0_START_INDEX + i);
+            if (otherEntryIndex == 0) continue; // No tree to duplicate
+            int entryIndexPointer = SET_0_START_INDEX + i;
+            for (i = getTopLevelShift(); i > other.getTopLevelShift(); i -= 4) {
+                // for each inserted level:
+
+                // Allocate entry in other:
+                int sizeOfEntry = NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + 1;
+                int newEntryIndex = 0;
+                do {
+                    try {
+                        newEntryIndex = newEntry(sizeOfEntry);
+                    } catch (ResizeException ex) {
+                        resizeArray(ex.getNewSize());
+                    }
+                }
+                while (newEntryIndex == 0);
+
+                // Link new level in.
+                setAtShortIndex(entryIndexPointer, (short) newEntryIndex);
+                // Populate new level entry, use pointer to slot 0 as place to populate under:
+                setPackedSlotIndicators(newEntryIndex, (short) 0x1); // Slot 0 populated
+                setPreviousVersionIndex(newEntryIndex, (short) 0); // No previous version
+                entryIndexPointer = newEntryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS; // Where the slot 0 index goes.
+            }
+            copyEntriesAtLevelFromOther(other, otherEntryIndex,
+                    entryIndexPointer, other.getTopLevelShift());
+        }
+    }
+
+    private void copyEntriesAtLevelFromOther(final AbstractPackedArrayContext other,
+                                             final int otherLevelEntryIndex,
+                                             final int levelEntryIndexPointer,
+                                             final int otherIndexShift) {
+        boolean nextLevelIsLeaf = (otherIndexShift == LEAF_LEVEL_SHIFT);
+        int packedSlotIndicators = other.getPackedSlotIndicators(otherLevelEntryIndex);
+        int numberOfSlots = Integer.bitCount(packedSlotIndicators);
+        int sizeOfEntry = NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + numberOfSlots;
+
+        // Allocate entry:
+        int entryIndex = 0;
+        do {
+            try {
+                entryIndex = newEntry(sizeOfEntry);
+            } catch (ResizeException ex) {
+                resizeArray(ex.getNewSize());
+            }
+        }
+        while (entryIndex == 0);
+
+        setAtShortIndex(levelEntryIndexPointer, (short) entryIndex);
+        setAtShortIndex(entryIndex + NON_LEAF_ENTRY_SLOT_INDICATORS_OFFSET, (short) packedSlotIndicators);
+        setAtShortIndex(entryIndex + NON_LEAF_ENTRY_PREVIOUS_VERSION_OFFSET, (short) 0);
+        for (int i = 0; i < numberOfSlots; i++) {
+            if (nextLevelIsLeaf) {
+                // Make leaf in other:
+                int leafEntryIndex = 0;
+                do {
+                    try {
+                        leafEntryIndex = newLeafEntry();
+                    } catch (ResizeException ex) {
+                        resizeArray(ex.getNewSize());
+                    }
+                }
+                while (leafEntryIndex == 0);
+                setIndexAtEntrySlot(entryIndex, i, (short) leafEntryIndex);
+                lazySetAtLongIndex(leafEntryIndex, 0);
+            } else {
+                int otherNextLevelEntryIndex = other.getIndexAtEntrySlot(otherLevelEntryIndex, i);
+                copyEntriesAtLevelFromOther(other, otherNextLevelEntryIndex,
+                        (entryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + i),
+                        otherIndexShift - 4);
+            }
+        }
+    }
+
+    //
+    //  #### ######## ######## ########     ###    ######## ####  #######  ##    ##
+    //   ##     ##    ##       ##     ##   ## ##      ##     ##  ##     ## ###   ##
+    //   ##     ##    ##       ##     ##  ##   ##     ##     ##  ##     ## ####  ##
+    //   ##     ##    ######   ########  ##     ##    ##     ##  ##     ## ## ## ##
+    //   ##     ##    ##       ##   ##   #########    ##     ##  ##     ## ##  ####
+    //   ##     ##    ##       ##    ##  ##     ##    ##     ##  ##     ## ##   ###
+    //  ####    ##    ######## ##     ## ##     ##    ##    ####  #######  ##    ##
+    //
+
+    // Recorded Value iteration:
+
+    private int seekToPopulatedVirtualIndexStartingAtLevel(final int startingVirtualIndex,
+                                                           final int levelEntryIndex,
+                                                           final int indexShift) throws RetryException {
+        int virtualIndex = startingVirtualIndex;
+        int firstVirtualIndexPastThisLevel = (((virtualIndex >>> indexShift) | 0xf) + 1) << indexShift;
+        boolean nextLevelIsLeaf = (indexShift == LEAF_LEVEL_SHIFT);
+        do {
+            // Target is a packedSlotIndicators entry
+            int packedSlotIndicators = getPackedSlotIndicators(levelEntryIndex);
+            int startingSlotBitNumber = (virtualIndex >>> indexShift) & 0xf;
+            int slotMask = 1 << startingSlotBitNumber;
+            int slotsAtAndAboveBitNumber = packedSlotIndicators & ~(slotMask - 1);
+            int nextActiveSlotBitNumber = Integer.numberOfTrailingZeros(slotsAtAndAboveBitNumber);
+
+
+            if (nextActiveSlotBitNumber > 15) {
+                // this level has no more set bits, pop back up a level.
+                int indexShiftAbove = indexShift + 4;
+                virtualIndex += 1 << indexShiftAbove;
+                virtualIndex &= ~((1 << indexShiftAbove) - 1); // Start at the beginning of the next slot a level above.
+                return -virtualIndex; // Negative value indicates a skip to a different index.
+            }
+
+            // Drill into bit.
+            if (nextActiveSlotBitNumber != startingSlotBitNumber) {
+                virtualIndex += (nextActiveSlotBitNumber - startingSlotBitNumber) << indexShift;
+                virtualIndex &= ~((1 << indexShift) - 1); // Start at the beginning of the next slot of this level
+            }
+
+            if (nextLevelIsLeaf) {
+                // There is recorded value here. No need to look.
+                return virtualIndex;
+            }
+
+            // Next level is not a leaf. Drill into it:
+
+            int nextSlotMask = 1 << nextActiveSlotBitNumber;
+            int slotsBelowNextBitNumber = packedSlotIndicators & (nextSlotMask - 1);
+            int nextSlotNumber = Integer.bitCount(slotsBelowNextBitNumber);
+
+            if ((packedSlotIndicators & nextSlotMask) == 0) {
+                throw new IllegalStateException("Unexpected 0 at slot index");
+            }
+
+            int entryPointerIndex = levelEntryIndex + NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS + nextSlotNumber;
+            int nextLevelEntryIndex = getIndexAtShortIndex(entryPointerIndex);
+            if (nextLevelEntryIndex == 0) {
+                throw new RetryException();
+            }
+            if (getPreviousVersionIndex(nextLevelEntryIndex) != 0) {
+                consolidateEntry(nextLevelEntryIndex);
+            }
+
+            virtualIndex = seekToPopulatedVirtualIndexStartingAtLevel(virtualIndex, nextLevelEntryIndex, indexShift - 4);
+            if (virtualIndex < 0) {
+                virtualIndex = -virtualIndex;
+            } else {
+                return virtualIndex;
+            }
+        } while (virtualIndex < firstVirtualIndexPastThisLevel);
+
+        return virtualIndex;
+    }
+
+    private int findFirstPotentiallyPopulatedVirtualIndexStartingAt(final int startingVirtualIndex) {
+        int nextVirtrualIndex = -1;
+        // Look for a populated virtual index in set 0:
+        boolean retry;
+        do {
+            retry = false;
+            try {
+                int entryIndex = getRootEntry(0);
+                if (entryIndex == 0) return getVirtualLength(); // Nothing under the root
+                nextVirtrualIndex =
+                        seekToPopulatedVirtualIndexStartingAtLevel(startingVirtualIndex, entryIndex, getTopLevelShift());
+            } catch (RetryException ex) {
+                retry = true;
+            }
+        } while (retry);
+
+        // Don't drill to value if out of range:
+        if ((nextVirtrualIndex < 0) || (nextVirtrualIndex >= getVirtualLength())) {
+            return getVirtualLength();
+        }
+
+        return nextVirtrualIndex;
+    }
+
+    // Recorded values iteration:
+
+    class NonZeroValuesIterator implements Iterator<IterationValue> {
+
+        int nextVirtrualIndex = 0;
+        long nextValue;
+
+        final IterationValue currentIterationValue = new IterationValue();
+
+        private void findFirstNonZeroValueVirtualIndexStartingAt(final int startingVirtualIndex) {
+            if (!isPacked()) {
+                // Look for non-zero value in unpacked context:
+                for (nextVirtrualIndex = startingVirtualIndex;
+                     nextVirtrualIndex < getVirtualLength();
+                     nextVirtrualIndex++) {
+                    if ((nextValue = getAtUnpackedIndex(nextVirtrualIndex)) != 0) {
+                        return;
+                    }
+                }
+                return;
+            }
+            // Context is packed:
+            nextVirtrualIndex = startingVirtualIndex;
+            do {
+                nextVirtrualIndex = findFirstPotentiallyPopulatedVirtualIndexStartingAt(nextVirtrualIndex);
+                if (nextVirtrualIndex >= getVirtualLength()) break;
+                if ((nextValue = contextLocalGetValueAtIndex(nextVirtrualIndex)) != 0) break;
+                nextVirtrualIndex++;
+            } while (true);
+        }
+
+        @Override
+        public IterationValue next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            currentIterationValue.set(nextVirtrualIndex, nextValue);
+            findFirstNonZeroValueVirtualIndexStartingAt(nextVirtrualIndex + 1);
+            return currentIterationValue;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return ((nextVirtrualIndex >= 0) &&
+                    (nextVirtrualIndex < getVirtualLength()));
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        NonZeroValuesIterator() {
+            findFirstNonZeroValueVirtualIndexStartingAt(0);
+        }
+    }
+
+    class NonZeroValues implements Iterable<IterationValue> {
+        public Iterator<IterationValue> iterator() {
+            return new NonZeroValuesIterator();
+        }
+    }
+
+    /**
+     * An Iterator over all non-Zero values in the array
+     * @return an Iterator over all non-Zero values in the array
+     */
+    Iterable<IterationValue> nonZeroValues() {
+        return new Iterable<IterationValue>() {
+            public Iterator<IterationValue> iterator() {
+                return new NonZeroValuesIterator();
+            }
+        };
+    }
+
+    //
+    //   ######  #### ######## ########      ####        ######  ##     ## #### ######## ########
+    //  ##    ##  ##       ##  ##           ##  ##      ##    ## ##     ##  ##  ##          ##
+    //  ##        ##      ##   ##            ####       ##       ##     ##  ##  ##          ##
+    //   ######   ##     ##    ######       ####         ######  #########  ##  ######      ##
+    //        ##  ##    ##     ##          ##  ## ##          ## ##     ##  ##  ##          ##
+    //  ##    ##  ##   ##      ##          ##   ##      ##    ## ##     ##  ##  ##          ##
+    //   ######  #### ######## ########     ####  ##     ######  ##     ## #### ##          ##
+    //
+
+    boolean isPacked() {
+        return isPacked;
+    }
+
+    int getPhysicalLength() {
+        return physicalLength;
+    }
+
+    int getVirtualLength() {
+        return virtualLength;
+    }
+
+    int determineTopLevelShiftForVirtualLength(final int virtualLength) {
+        int sizeMagnitude = (int) Math.ceil(Math.log(virtualLength) / Math.log(2));
+        int eightsSizeMagnitude = sizeMagnitude - 3;
+        int multipleOfFourSizeMagnitude = (int) Math.ceil(eightsSizeMagnitude / 4.0) * 4;
+        multipleOfFourSizeMagnitude = Math.max(multipleOfFourSizeMagnitude, 8);
+        int topLevelShiftNeeded = (multipleOfFourSizeMagnitude - 4) + 3;
+        return topLevelShiftNeeded;
+    }
+
+    void setVirtualLength(final int virtualLength) {
+        if (!isPacked()) {
+            throw new IllegalStateException("Should never be adjusting the virtual size of a non-packed context");
+        }
+        int newTopLevelShift = determineTopLevelShiftForVirtualLength(virtualLength);
+        setTopLevelShift(newTopLevelShift);
+        this.virtualLength = virtualLength;
+    }
+
+    int getTopLevelShift() {
+        return topLevelShift;
+    }
+
+    private void setTopLevelShift(final int topLevelShift) {
+        this.topLevelShift = topLevelShift;
+    }
+
+    int getPopulatedLongLength() {
+        return (getPopulatedShortLength() + 3) >> 2; // round up
+    }
+
+    int getPopulatedByteLength() {
+        return getPopulatedShortLength() << 1;
+    }
+
+    //
+    //   ########  #######           ######  ######## ########  #### ##    ##  ######
+    //      ##    ##     ##         ##    ##    ##    ##     ##  ##  ###   ## ##    ##
+    //      ##    ##     ##         ##          ##    ##     ##  ##  ####  ## ##
+    //      ##    ##     ## #######  ######     ##    ########   ##  ## ## ## ##   ####
+    //      ##    ##     ##               ##    ##    ##   ##    ##  ##  #### ##    ##
+    //      ##    ##     ##         ##    ##    ##    ##    ##   ##  ##   ### ##    ##
+    //      ##     #######           ######     ##    ##     ## #### ##    ##  ######
+    //
+
+    private String nonLeafEntryToString(final int entryIndex,
+                                        final int indexShift,
+                                        final int indentLevel) {
+        String output = "";
+        for (int i = 0; i < indentLevel; i++) {
+            output += "  ";
+        }
+        try {
+            final int packedSlotIndicators = getPackedSlotIndicators(entryIndex);
+            output += String.format("slotIndiators: 0x%02x, prevVersionIndex: %3d: [ ",
+                    packedSlotIndicators,
+                    getPreviousVersionIndex(entryIndex));
+            final int numberOfslotsInEntry = Integer.bitCount(packedSlotIndicators);
+            for (int i = 0; i < numberOfslotsInEntry; i++) {
+                output += String.format("%d", getIndexAtEntrySlot(entryIndex, i));
+                if (i < numberOfslotsInEntry - 1) {
+                    output += ", ";
+                }
+            }
+            output += String.format(" ] (indexShift = %d)\n", indexShift);
+            final boolean nextLevelIsLeaf = (indexShift == LEAF_LEVEL_SHIFT);
+            for (int i = 0; i < numberOfslotsInEntry; i++) {
+                final int nextLevelEntryIndex = getIndexAtEntrySlot(entryIndex, i);
+                if (nextLevelIsLeaf) {
+                    output += leafEntryToString(nextLevelEntryIndex, indentLevel + 4);
+                } else {
+                    output += nonLeafEntryToString(nextLevelEntryIndex,
+                            indexShift - 4, indentLevel + 4);
+                }
+            }
+        } catch (Exception ex) {
+            output += String.format("Exception thrown at nonLeafEnty at index %d with indexShift %d\n",
+                    entryIndex, indexShift);
+        }
+        return output;
+    }
+
+    private String leafEntryToString(final int entryIndex, final int indentLevel) {
+        String output = "";
+        for (int i = 0; i < indentLevel; i++) {
+            output += "  ";
+        }
+        try {
+            output += "Leaf bytes : ";
+            for (int i = 56; i >= 0; i -= 8) {
+                output += String.format("0x%02x ", (getAtLongIndex(entryIndex) >>> i) & 0xff);
+
+            }
+            output += "\n";
+        } catch (Exception ex) {
+            output += String.format("Exception thrown at leafEnty at index %d\n", entryIndex);
+        }
+        return output;
+    }
+
+    private String recordedValuesToString() {
+        String output = "";
+        try {
+            for (IterationValue v : nonZeroValues()) {
+                output += String.format("[%d] : %d\n", v.getIndex(), v.getValue());
+            }
+            return output;
+        } catch(Exception ex) {
+            output += "!!! Exception thown in value iteration...\n";
+        }
+        return output;
+    }
+
+    @Override
+    public String toString() {
+        String output = "PackedArrayContext:\n";
+        if (!isPacked()) {
+            return output + "Context is unpacked:\n" + unpackedToString();
+        }
+        for (int setNumber = 0; setNumber < NUMBER_OF_SETS; setNumber++) {
+            try {
+                int entryPointerIndex = SET_0_START_INDEX + setNumber;
+                int entryIndex = getIndexAtShortIndex(entryPointerIndex);
+                output += String.format("Set %d: root = %d \n", setNumber, entryIndex);
+                if (entryIndex == 0) continue;
+                output += nonLeafEntryToString(entryIndex, getTopLevelShift(), 4);
+            } catch (Exception ex) {
+                output += String.format("Exception thrown in set %d\n", setNumber);
+            }
+        }
+        output += recordedValuesToString();
+        return output;
+    }
+
+    private static class RetryException extends Exception { }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/AbstractPackedLongArray.java b/src/main/java/org/HdrHistogram/packedarray/AbstractPackedLongArray.java
new file mode 100644
index 0000000..cefd89e
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/AbstractPackedLongArray.java
@@ -0,0 +1,406 @@
+package org.HdrHistogram.packedarray;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * A Packed array of signed 64 bit values, and supports {@link #get get()}, {@link #set set()},
+ * {@link #add add()} and {@link #increment increment()} operations on the logical contents of the array.
+ */
+abstract class AbstractPackedLongArray implements Iterable<Long>, Serializable {
+    /**
+     * An {@link AbstractPackedLongArray} Uses {@link AbstractPackedArrayContext} to track
+     * the array's logical contents. Contexts may be switched when a context requires resizing
+     * to complete logical array operations (get, set, add, increment). Contexts are
+     * established and used within critical sections in order to facilitate concurrent
+     * implementors.
+     */
+
+    private static final int NUMBER_OF_SETS = 8;
+
+    private AbstractPackedArrayContext arrayContext;
+    private long startTimeStampMsec = Long.MAX_VALUE;
+    private long endTimeStampMsec = 0;
+
+    AbstractPackedArrayContext getArrayContext() {
+        return arrayContext;
+    }
+
+    void setArrayContext(AbstractPackedArrayContext newArrayContext) {
+        arrayContext = newArrayContext;
+    }
+
+    /**
+     * get the start time stamp [optionally] stored with this array
+     * @return the start time stamp [optionally] stored with this array
+     */
+    public long getStartTimeStamp() {
+        return startTimeStampMsec;
+    }
+
+    /**
+     * Set the start time stamp value associated with this array to a given value.
+     * @param timeStampMsec the value to set the time stamp to, [by convention] in msec since the epoch.
+     */
+    public void setStartTimeStamp(final long timeStampMsec) {
+        this.startTimeStampMsec = timeStampMsec;
+    }
+
+    /**
+     * get the end time stamp [optionally] stored with this array
+     * @return the end time stamp [optionally] stored with this array
+     */
+    public long getEndTimeStamp() {
+        return endTimeStampMsec;
+    }
+
+    /**
+     * Set the end time stamp value associated with this array to a given value.
+     * @param timeStampMsec the value to set the time stamp to, [by convention] in msec since the epoch.
+     */
+    public void setEndTimeStamp(final long timeStampMsec) {
+        this.endTimeStampMsec = timeStampMsec;
+    }
+
+    /**
+     * Set a new virtual length for the array.
+     * @param newVirtualArrayLength the
+     */
+    abstract public void setVirtualLength(final int newVirtualArrayLength);
+
+    /**
+     * Create a copy of this array, complete with data and everything.
+     *
+     * @return A distinct copy of this array.
+     */
+    abstract public AbstractPackedLongArray copy();
+
+    abstract void resizeStorageArray(int newPhysicalLengthInLongs);
+
+    abstract void clearContents();
+
+    abstract long criticalSectionEnter();
+
+    abstract void criticalSectionExit(long criticalValueAtEnter);
+
+
+    @Override
+    public String toString() {
+        String output = "PackedArray:\n";
+        AbstractPackedArrayContext arrayContext = getArrayContext();
+        output += arrayContext.toString();
+        return output;
+    }
+
+    /**
+     * Get value at virtual index in the array
+     * @param index the virtual array index
+     * @return the array value at the virtual index given
+     */
+    public long get(final int index) {
+        long value = 0;
+        for (int byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum ++) {
+            int packedIndex = 0;
+            long byteValueAtPackedIndex = 0;
+            do {
+                int newArraySize = 0;
+                long criticalValue = criticalSectionEnter();
+                try {
+                    // Establish context within: critical section
+                    AbstractPackedArrayContext arrayContext = getArrayContext();
+                    // Deal with unpacked context:
+                    if (!arrayContext.isPacked()) {
+                        return arrayContext.getAtUnpackedIndex(index);
+                    }
+                    // Context is packed:
+                    packedIndex = arrayContext.getPackedIndex(byteNum, index, false);
+                    if (packedIndex < 0) {
+                        return value;
+                    }
+                    byteValueAtPackedIndex =
+                            (((long)arrayContext.getAtByteIndex(packedIndex)) & 0xff) << (byteNum << 3);
+                } catch (ResizeException ex) {
+                    newArraySize = ex.getNewSize(); // Resize outside of critical section
+                } finally {
+                    criticalSectionExit(criticalValue);
+                    if (newArraySize != 0) {
+                        resizeStorageArray(newArraySize);
+                    }
+                }
+            } while (packedIndex == 0);
+
+            value += byteValueAtPackedIndex;
+        }
+        return value;
+    }
+
+    /**
+     * Increment value at a virrual index in the array
+     * @param index virtual index of value to increment
+     */
+    public void increment(final int index) {
+        add(index, 1);
+    }
+
+    /**
+     * Add to a value at a virtual index in the array
+     * @param index the virtual index of the value to be added to
+     * @param value the value to add
+     */
+    public void add(final int index, final long value) {
+        if (value == 0) {
+            return;
+        }
+        long remainingValueToAdd = value;
+
+        do {
+            try {
+                long byteMask = 0xff;
+                for (int byteNum = 0, byteShift = 0;
+                     byteNum < NUMBER_OF_SETS;
+                     byteNum++, byteShift += 8, byteMask <<= 8) {
+                    final long criticalValue = criticalSectionEnter();
+                    try {
+                        // Establish context within: critical section
+                        AbstractPackedArrayContext arrayContext = getArrayContext();
+                        // Deal with unpacked context:
+                        if (!arrayContext.isPacked()) {
+                            arrayContext.addAndGetAtUnpackedIndex(index, remainingValueToAdd);
+                            return;
+                        }
+                        // Context is packed:
+                        int packedIndex = arrayContext.getPackedIndex(byteNum, index, true);
+
+                        long amountToAddAtSet = remainingValueToAdd & byteMask;
+                        byte byteToAdd = (byte) (amountToAddAtSet >> byteShift);
+                        long afterAddByteValue = arrayContext.addAtByteIndex(packedIndex, byteToAdd);
+
+                        // Reduce remaining value to add by amount just added:
+                        remainingValueToAdd -= amountToAddAtSet;
+
+                        // Account for carry:
+                        long carryAmount = afterAddByteValue & 0x100;
+                        remainingValueToAdd += carryAmount << byteShift;
+
+                        if (remainingValueToAdd == 0) {
+                            return; // nothing to add to higher magnitudes
+                        }
+                    } finally {
+                        criticalSectionExit(criticalValue);
+
+                    }
+                }
+                return;
+            } catch (ResizeException ex){
+                resizeStorageArray(ex.getNewSize()); // Resize outside of critical section
+            }
+        } while (true);
+    }
+
+    /**
+     * Set the value at a virtual index in the array
+     * @param index the virtual index of the value to set
+     * @param value the value to set
+     */
+    public void set(final int index, final long value) {
+        int bytesAlreadySet = 0;
+        do {
+            long valueForNextLevels = value;
+            try {
+                for (int byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) {
+                    long criticalValue = criticalSectionEnter();
+                    try {
+                        // Establish context within: critical section
+                        AbstractPackedArrayContext arrayContext = getArrayContext();
+                        // Deal with unpacked context:
+                        if (!arrayContext.isPacked()) {
+                            arrayContext.setAtUnpackedIndex(index, value);
+                            return;
+                        }
+                        // Context is packed:
+                        if (valueForNextLevels == 0) {
+                            // Special-case zeros to avoid inflating packed array for no reason
+                            int packedIndex = arrayContext.getPackedIndex(byteNum, index, false);
+                            if (packedIndex < 0) {
+                                return; // no need to create entries for zero values if they don't already exist
+                            }
+                        }
+                        // Make sure byte is populated:
+                        int packedIndex = arrayContext.getPackedIndex(byteNum, index, true);
+
+                        // Determine value to write, and prepare for next levels
+                        byte byteToWrite = (byte) (valueForNextLevels & 0xff);
+                        valueForNextLevels >>= 8;
+
+                        if (byteNum < bytesAlreadySet) {
+                            // We want to avoid writing to the same byte twice when not doing so for the
+                            // entire 64 bit value atomically, as doing so opens a race with e.g. concurrent
+                            // adders. So dobn't actually write the byte if has been written before.
+                            continue;
+                        }
+                        arrayContext.setAtByteIndex(packedIndex, byteToWrite);
+                        bytesAlreadySet++;
+                    } finally {
+                        criticalSectionExit(criticalValue);
+                    }
+                }
+                return;
+            } catch (ResizeException ex) {
+                resizeStorageArray(ex.getNewSize()); // Resize outside of critical section
+            }
+        } while (true);
+    }
+
+    /**
+     * Add the contents of the other array to this one
+     *
+     * @param other The to add to this array
+     */
+    public void add(final AbstractPackedLongArray other) {
+        for (IterationValue v : other.nonZeroValues()) {
+            add(v.getIndex(), v.getValue());
+        }
+    }
+
+    /**
+     * Clear the array contents
+     */
+    public void clear() {
+        clearContents();
+    }
+
+    /**
+     * Get the current physical length (in longs) of the array's backing storage
+     * @return the current physical length (in longs) of the array's current backing storage
+     */
+    public int getPhysicalLength() {
+        return getArrayContext().length();
+    }
+
+    /**
+     * Get the (virtual) length of the array
+     * @return the (virtual) length of the array
+     */
+    public int length() {
+        return getArrayContext().getVirtualLength();
+    }
+
+    // Regular array iteration (iterates over all virtrual indexes, zero-value or not:
+
+    class AllValuesIterator implements Iterator<Long> {
+
+        int nextVirtrualIndex = 0;
+
+        @Override
+        public Long next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            return get(nextVirtrualIndex++);
+        }
+
+        @Override
+        public boolean hasNext() {
+            return ((nextVirtrualIndex >= 0) &&
+                    (nextVirtrualIndex < length()));
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    /**
+     * An Iterator over all values in the array
+     * @return an Iterator over all values in the array
+     */
+    public Iterator<Long> iterator() {
+        return new AllValuesIterator();
+    }
+
+    /**
+     * An Iterator over all non-Zero values in the array
+     * @return an Iterator over all non-Zero values in the array
+     */
+    public Iterable<IterationValue> nonZeroValues() {
+        return getArrayContext().nonZeroValues();
+    }
+
+    /**
+     * Determine if this array is equivalent to another.
+     *
+     * @param other the other array to compare to
+     * @return True if this array are equivalent with the other.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof AbstractPackedLongArray)) {
+            return false;
+        }
+        AbstractPackedLongArray that = (AbstractPackedLongArray) other;
+        if (length() != that.length()) {
+            return false;
+        }
+        if (this.arrayContext.isPacked() || that.arrayContext.isPacked()) {
+            // If at least one of the arrays is packed, comparing only the
+            // non-zero values that exist in both arrays, using two passes,
+            // will likely be more efficient than a single all-index pass:
+            // - If both are packed, it will obvioulsy be much faster.
+            // - If one is packed and the other is not, we would be visiting
+            //   every index in the non-packed array, in one of the passes,
+            //   but would still only visit the non-zero elements in the
+            //   packed one.
+            for (IterationValue v : this.nonZeroValues()) {
+                if (that.get(v.getIndex()) != v.getValue()) {
+                    return false;
+                }
+            }
+            for (IterationValue v : that.nonZeroValues()) {
+                if (this.get(v.getIndex()) != v.getValue()) {
+                    return false;
+                }
+            }
+        } else {
+            for (int i = 0; i < this.length(); i++) {
+                if (this.get(i) != that.get(i)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    static final int NUMBER_OF_NON_ZEROS_TO_HASH = 8;
+
+    @Override
+    public int hashCode() {
+        int h = 0;
+        h = oneAtATimeHashStep(h, length());
+        int count = 0;
+        // Include the first NUMBER_OF_NON_ZEROS_TO_HASH non-zeros in the hash:
+        for (IterationValue v : nonZeroValues()) {
+            if (++count > NUMBER_OF_NON_ZEROS_TO_HASH) {
+                break;
+            }
+            h = oneAtATimeHashStep(h, (int) v.getIndex());
+            h = oneAtATimeHashStep(h, (int) v.getValue());
+        }
+        h += (h << 3);
+        h ^= (h >> 11);
+        h += (h << 15);
+        return h;
+    }
+
+    private int oneAtATimeHashStep(final int incomingHash, final int v) {
+        int h = incomingHash;
+        h += v;
+        h += (h << 10);
+        h ^= (h >> 6);
+        return h;
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/ConcurrentPackedArrayContext.java b/src/main/java/org/HdrHistogram/packedarray/ConcurrentPackedArrayContext.java
new file mode 100644
index 0000000..ba5bb62
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/ConcurrentPackedArrayContext.java
@@ -0,0 +1,124 @@
+package org.HdrHistogram.packedarray;
+
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicLongArray;
+
+
+class ConcurrentPackedArrayContext extends PackedArrayContext {
+
+    ConcurrentPackedArrayContext(final int virtualLength,
+                                 final int initialPhysicalLength,
+                                 final boolean allocateArray) {
+        super(virtualLength, initialPhysicalLength, false);
+        if (allocateArray) {
+            array = new AtomicLongArray(getPhysicalLength());
+            init(virtualLength);
+        }
+    }
+
+    ConcurrentPackedArrayContext(final int virtualLength,
+                                 final int initialPhysicalLength) {
+        this(virtualLength, initialPhysicalLength, true);
+    }
+
+    ConcurrentPackedArrayContext(final int newVirtualCountsArraySize,
+                                 final AbstractPackedArrayContext from,
+                                 final int arrayLength) {
+        this(newVirtualCountsArraySize, arrayLength);
+        if (isPacked()) {
+            populateEquivalentEntriesWithZerosFromOther(from);
+        }
+    }
+
+    private AtomicLongArray array;
+    private volatile int populatedShortLength;
+
+    private static final AtomicIntegerFieldUpdater<ConcurrentPackedArrayContext> populatedShortLengthUpdater =
+            AtomicIntegerFieldUpdater.newUpdater(ConcurrentPackedArrayContext.class, "populatedShortLength");
+
+    @Override
+    int length() {
+        return array.length();
+    }
+
+    @Override
+    int getPopulatedShortLength() {
+        return populatedShortLength;
+    }
+
+    @Override
+    boolean casPopulatedShortLength(final int expectedPopulatedShortLength, final int newPopulatedShortLength) {
+        return populatedShortLengthUpdater.compareAndSet(this, expectedPopulatedShortLength, newPopulatedShortLength);
+    }
+
+    @Override
+    boolean casPopulatedLongLength(final int expectedPopulatedLongLength, final int newPopulatedLongLength) {
+        int existingShortLength = getPopulatedShortLength();
+        int existingLongLength = (existingShortLength + 3) >> 2;
+        if (existingLongLength != expectedPopulatedLongLength) return false;
+        return casPopulatedShortLength(existingShortLength, newPopulatedLongLength << 2);
+    }
+
+    @Override
+    long getAtLongIndex(final int longIndex) {
+        return array.get(longIndex);
+    }
+
+    @Override
+    boolean casAtLongIndex(final int longIndex, final long expectedValue, final long newValue) {
+        return array.compareAndSet(longIndex, expectedValue, newValue);
+    }
+
+    @Override
+    void lazySetAtLongIndex(final int longIndex, final long newValue) {
+        array.lazySet(longIndex, newValue);
+    }
+
+    @Override
+    void clearContents() {
+        for (int i = 0; i < array.length(); i++) {
+            array.lazySet(i, 0);
+        }
+        init(getVirtualLength());
+    }
+
+    @Override
+    void resizeArray(final int newLength) {
+        final AtomicLongArray newArray = new AtomicLongArray(newLength);
+        int copyLength = Math.min(array.length(), newLength);
+        for (int i = 0; i < copyLength; i++) {
+            newArray.lazySet(i, array.get(i));
+        }
+        array = newArray;
+    }
+
+    @Override
+    long getAtUnpackedIndex(final int index) {
+        return array.get(index);
+    }
+
+    @Override
+    void setAtUnpackedIndex(final int index, final long newValue) {
+        array.set(index, newValue);
+    }
+
+    @Override
+    void lazysetAtUnpackedIndex(final int index, final long newValue) {
+        array.lazySet(index, newValue);
+    }
+
+    @Override
+    long incrementAndGetAtUnpackedIndex(final int index) {
+        return array.incrementAndGet(index);
+    }
+
+    @Override
+    long addAndGetAtUnpackedIndex(final int index, final long valueToAdd) {
+        return array.addAndGet(index, valueToAdd);
+    }
+
+    @Override
+    String unpackedToString() {
+        return array.toString();
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/ConcurrentPackedLongArray.java b/src/main/java/org/HdrHistogram/packedarray/ConcurrentPackedLongArray.java
new file mode 100644
index 0000000..dafe19d
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/ConcurrentPackedLongArray.java
@@ -0,0 +1,168 @@
+package org.HdrHistogram.packedarray;
+
+import org.HdrHistogram.WriterReaderPhaser;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+
+/**
+ * A Packed array of signed 64 bit values that supports {@link #get get()}, {@link #set set()},
+ * {@link #add add()} and {@link #increment increment()} operations the logical contents of the array.
+ * <p>
+ * {@link ConcurrentPackedLongArray} supports concurrent accumulation, with the {@link #add add()}
+ * and {@link #increment increment()} methods providing lossless atomic accumulation in the presence of
+ * multiple writers. However, it is impotant to note that {@link #add add()} and {@link #increment increment()}
+ * are the *only* safe concurrent operations, and that all other operations, including
+ * {@link #get get()}, {@link #set set()} and {@link #clear()} may produce "suprising" results if used on an
+ * array that is not at rest.
+ *
+ * While the {@link #add add()} and {@link #increment increment()} methods are not quite wait-free, they
+ * come "close" that behvaior in the sense that a given thread will incur a total of no more than a capped
+ * fixed number (e.g. 74 in a current implementation) of non-wait-free add or increment operations during
+ * the lifetime of an array, regradless of the number of operations done.
+ * </p>
+ */
+public class ConcurrentPackedLongArray extends PackedLongArray {
+
+    public ConcurrentPackedLongArray(final int virtualLength) {
+        this(virtualLength, AbstractPackedArrayContext.MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY);
+    }
+
+    public ConcurrentPackedLongArray(final int virtualLength, final int initialPhysicalLength) {
+        super();
+        setArrayContext(new ConcurrentPackedArrayContext(virtualLength, initialPhysicalLength));
+    }
+
+    transient WriterReaderPhaser wrp = new WriterReaderPhaser();
+
+    @Override
+    void resizeStorageArray(final int newPhysicalLengthInLongs) {
+        AbstractPackedArrayContext inactiveArrayContext;
+        try {
+            wrp.readerLock();
+
+            ConcurrentPackedArrayContext newArrayContext =
+                    new ConcurrentPackedArrayContext(
+                            getArrayContext().getVirtualLength(),
+                            getArrayContext(), newPhysicalLengthInLongs
+                    );
+
+            // Flip the current live array context and the newly created one:
+            inactiveArrayContext = getArrayContext();
+            setArrayContext(newArrayContext);
+
+            wrp.flipPhase();
+
+            // The now inactive array context is stable, and the new array context is active.
+            // We don't want to try to record values from the inactive into the new array context
+            // here (under the wrp reader lock) because we could deadlock if resizing is needed.
+            // Instead, value recording will be done after we release the read lock.
+
+        } finally {
+            wrp.readerUnlock();
+        }
+
+        // Record all contents from the now inactive array to new live one:
+        for (IterationValue v : inactiveArrayContext.nonZeroValues()) {
+            add(v.getIndex(), v.getValue());
+        }
+
+        // inactive array contents is fully committed into the newly resized live array. It can now die in peace.
+
+    }
+
+    @Override
+    public void setVirtualLength(final int newVirtualArrayLength) {
+        if (newVirtualArrayLength < length()) {
+            throw new IllegalArgumentException(
+                    "Cannot set virtual length, as requested length " + newVirtualArrayLength +
+                            " is smaller than the current virtual length " + length());
+        }
+        AbstractPackedArrayContext inactiveArrayContext;
+        try {
+            wrp.readerLock();
+            AbstractPackedArrayContext currentArrayContext = getArrayContext();
+            if (currentArrayContext.isPacked() &&
+                    (currentArrayContext.determineTopLevelShiftForVirtualLength(newVirtualArrayLength) ==
+                            currentArrayContext.getTopLevelShift())) {
+                // No changes to the array context contents is needed. Just change the virtual length.
+                currentArrayContext.setVirtualLength(newVirtualArrayLength);
+                return;
+            }
+            inactiveArrayContext = currentArrayContext;
+            setArrayContext(
+                    new ConcurrentPackedArrayContext(
+                            newVirtualArrayLength,
+                            inactiveArrayContext,
+                            inactiveArrayContext.length()
+                    ));
+
+            wrp.flipPhase();
+
+            // The now inactive array context is stable, and the new array context is active.
+            // We don't want to try to record values from the inactive into the new array context
+            // here (under the wrp reader lock) because we could deadlock if resizing is needed.
+            // Instead, value recording will be done after we release the read lock.
+
+        } finally {
+            wrp.readerUnlock();
+        }
+
+        for (IterationValue v : inactiveArrayContext.nonZeroValues()) {
+            add(v.getIndex(), v.getValue());
+        }
+    }
+
+    @Override
+    public ConcurrentPackedLongArray copy() {
+        ConcurrentPackedLongArray copy = new ConcurrentPackedLongArray(this.length(), this.getPhysicalLength());
+        copy.add(this);
+        return copy;
+    }
+
+    @Override
+    void clearContents() {
+        try {
+            wrp.readerLock();
+            getArrayContext().clearContents();
+        } finally {
+            wrp.readerUnlock();
+        }
+    }
+
+    @Override
+    long criticalSectionEnter() {
+        return wrp.writerCriticalSectionEnter();
+    }
+
+    @Override
+    void criticalSectionExit(long criticalValueAtEnter) {
+        wrp.writerCriticalSectionExit(criticalValueAtEnter);
+    }
+
+    @Override
+    public String toString() {
+        try {
+            wrp.readerLock();
+            return super.toString();
+        } finally {
+            wrp.readerUnlock();
+        }
+    }
+
+    @Override
+    public void clear() {
+        try {
+            wrp.readerLock();
+            super.clear();
+        } finally {
+            wrp.readerUnlock();
+        }
+    }
+
+    private void readObject(final ObjectInputStream o)
+            throws IOException, ClassNotFoundException {
+        o.defaultReadObject();
+        wrp = new WriterReaderPhaser();
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/IterationValue.java b/src/main/java/org/HdrHistogram/packedarray/IterationValue.java
new file mode 100644
index 0000000..063f6ab
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/IterationValue.java
@@ -0,0 +1,33 @@
+package org.HdrHistogram.packedarray;
+
+/**
+ * An iteration value representing the index iterated to, and the value found at that index
+ */
+public class IterationValue {
+    IterationValue() {
+    }
+
+    void set(final int index, final long value) {
+        this.index = index;
+        this.value = value;
+    }
+
+    /**
+     * The index iterated to
+     * @return the index iterated to
+     */
+    public int getIndex() {
+        return index;
+    }
+
+    /**
+     * The value at the index iterated to
+     * @return the value at the index iterated to
+     */
+    public long getValue() {
+        return value;
+    }
+
+    private int index;
+    private long value;
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/PackedArrayContext.java b/src/main/java/org/HdrHistogram/packedarray/PackedArrayContext.java
new file mode 100644
index 0000000..b415a86
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/PackedArrayContext.java
@@ -0,0 +1,119 @@
+package org.HdrHistogram.packedarray;
+
+import java.util.Arrays;
+
+/**
+ * A non-concurrent array context. No atomics used.
+ */
+class PackedArrayContext extends AbstractPackedArrayContext {
+
+    PackedArrayContext(final int virtualLength,
+                       final int initialPhysicalLength,
+                       final boolean allocateArray) {
+        super(virtualLength, initialPhysicalLength);
+        if (allocateArray) {
+            array = new long[getPhysicalLength()];
+            init(virtualLength);
+        }
+    }
+
+    PackedArrayContext(final int virtualLength,
+                       final int initialPhysicalLength) {
+        this(virtualLength, initialPhysicalLength, true);
+    }
+
+    PackedArrayContext(final int virtualLength,
+                       final AbstractPackedArrayContext from,
+                       final int newPhysicalArrayLength) {
+        this(virtualLength, newPhysicalArrayLength);
+        if (isPacked()) {
+            populateEquivalentEntriesWithZerosFromOther(from);
+        }
+    }
+
+    private long[] array;
+    private int populatedShortLength = 0;
+
+    @Override
+    int length() {
+        return array.length;
+    }
+
+    @Override
+    int getPopulatedShortLength() {
+        return populatedShortLength;
+    }
+
+    @Override
+    boolean casPopulatedShortLength(final int expectedPopulatedShortLength, final int newPopulatedShortLength) {
+        if (this.populatedShortLength != expectedPopulatedShortLength) return false;
+        this.populatedShortLength = newPopulatedShortLength;
+        return true;
+    }
+
+    @Override
+    boolean casPopulatedLongLength(final int expectedPopulatedLongLength, final int newPopulatedLongLength) {
+        if (getPopulatedLongLength() != expectedPopulatedLongLength) return false;
+        return casPopulatedShortLength(populatedShortLength, newPopulatedLongLength << 2);
+    }
+
+    @Override
+    long getAtLongIndex(final int longIndex) {
+        return array[longIndex];
+    }
+
+    @Override
+    boolean casAtLongIndex(final int longIndex, final long expectedValue, final long newValue) {
+        if (array[longIndex] != expectedValue) return false;
+        array[longIndex] = newValue;
+        return true;
+    }
+
+    @Override
+    void lazySetAtLongIndex(final int longIndex, final long newValue) {
+        array[longIndex] = newValue;
+    }
+
+    @Override
+    void clearContents() {
+        java.util.Arrays.fill(array, 0);
+        init(getVirtualLength());
+    }
+
+    @Override
+    void resizeArray(final int newLength) {
+        array = Arrays.copyOf(array, newLength);
+    }
+
+    @Override
+    long getAtUnpackedIndex(final int index) {
+        return array[index];
+    }
+
+    @Override
+    void setAtUnpackedIndex(final int index, final long newValue) {
+        array[index] = newValue;
+    }
+
+    @Override
+    void lazysetAtUnpackedIndex(final int index, final long newValue) {
+        array[index] = newValue;
+    }
+
+    @Override
+    long incrementAndGetAtUnpackedIndex(final int index) {
+        array[index]++;
+        return array[index];
+    }
+
+    @Override
+    long addAndGetAtUnpackedIndex(final int index, final long valueToAdd) {
+        array[index] += valueToAdd;
+        return array[index];
+    }
+
+    @Override
+    String unpackedToString() {
+        return Arrays.toString(array);
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/PackedArrayRecorder.java b/src/main/java/org/HdrHistogram/packedarray/PackedArrayRecorder.java
new file mode 100644
index 0000000..35d495f
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/PackedArrayRecorder.java
@@ -0,0 +1,296 @@
+/**
+ * Written by Gil Tene of Azul Systems, and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * @author Gil Tene
+ */
+
+package org.HdrHistogram.packedarray;
+
+import org.HdrHistogram.*;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Records increments and adds of integer values at indexes of a logical array of 64 bit signed integer values, and
+ * provides stable interval {@link PackedLongArray} samples from live recorded data without interrupting or stalling
+ * active recording of values. Each interval array provided contains all values accumulated since the previous
+ * interval array was taken.
+ * <p>
+ * This pattern is commonly used in logging interval accumulator information while recording is ongoing.
+ * <p>
+ * {@link PackedArrayRecorder} supports fully concurrent
+ * {@link PackedArrayRecorder#increment(int)} and
+ * {@link PackedArrayRecorder#add(int, long)} calls.
+ * While the {@link #increment increment()} and {@link #add add()} methods are not quite wait-free, they
+ * come "close" to that behvaior in the sense that a given thread will incur a total of no more than a capped
+ * fixed number (e.g. 74 in a current implementation) of non-wait-free add or increment operations during
+ * the lifetime of an interval array (including across recycling of that array across intervals within the
+ * same recorder), regradless of the number of operations done.
+ * <p>
+ * A common pattern for using a {@link PackedArrayRecorder} looks like this:
+ * <br><pre><code>
+ * PackedArrayRecorder recorder = new PackedArrayRecorder(); //
+ * PackedLongArray intervalArray = null;
+ * ...
+ * [start of some loop construct that periodically wants to grab an interval array]
+ *   ...
+ *   // Get interval array, recycling previous interval array:
+ *   intervalArray = recorder.getIntervalArray(intervalArray);
+ *   // Process the interval array, which is nice and stable here:
+ *   myLogWriter.logArrayContents(intervalArray);
+ *   ...
+ * [end of loop construct]
+ * </code></pre>
+ *
+ */
+
+public class PackedArrayRecorder {
+    private static AtomicLong instanceIdSequencer = new AtomicLong(1);
+    private final long instanceId = instanceIdSequencer.getAndIncrement();
+
+    private final WriterReaderPhaser recordingPhaser = new WriterReaderPhaser();
+
+    private volatile PackedLongArray activeArray;
+
+    /**
+     * Construct a {@link PackedArrayRecorder} with a given (virtual) array length.
+     *
+     * @param virtualLength The (virtual) array length
+     */
+    public PackedArrayRecorder(final int virtualLength) {
+        activeArray = new InternalConcurrentPackedLongArray(instanceId, virtualLength);
+        activeArray.setStartTimeStamp(System.currentTimeMillis());
+    }
+
+    /**
+     * Construct a {@link PackedArrayRecorder} with a given (virtual) array length, starting with a given
+     * initial physical backing store length
+     *
+     * @param virtualLength The (virtual) array length
+     * @param initialPhysicalLength The initial physical backing store length
+     */
+    public PackedArrayRecorder(final int virtualLength, final int initialPhysicalLength) {
+        activeArray = new InternalConcurrentPackedLongArray(instanceId, virtualLength, initialPhysicalLength);
+        activeArray.setStartTimeStamp(System.currentTimeMillis());
+    }
+
+    /**
+     * Returns the virtual length of the array represented by this recorder
+     * @return The virtual length of the array represented by this recorder
+     */
+    public int length() {
+        return activeArray.length();
+    }
+
+    /**
+     * Change the (virtual) length of the array represented by the this recorder
+     * @param newVirtualLength the new (virtual) length to use
+     */
+    public void setVirtualLength(int newVirtualLength) {
+        try {
+            recordingPhaser.readerLock();
+            // We don't care about concurrent modifications to the array, as setVirtualLength() in the
+            // ConcurrentPackedLongArray takes care of those. However, we must perform the change of virtual
+            // length under the recorder's readerLock proptection to prevent mid-change observations:
+            activeArray.setVirtualLength(newVirtualLength);
+        } finally {
+            recordingPhaser.readerUnlock();
+        }
+    }
+
+    /**
+     * Increment a value at a given index in the array
+     * @param index the index of trhe value to be incremented
+     * @throws ArrayIndexOutOfBoundsException (may throw) if value is exceeds length()
+     */
+    public void increment(final int index) throws ArrayIndexOutOfBoundsException {
+        long criticalValueAtEnter = recordingPhaser.writerCriticalSectionEnter();
+        try {
+            activeArray.increment(index);
+        } finally {
+            recordingPhaser.writerCriticalSectionExit(criticalValueAtEnter);
+        }
+    }
+
+    /**
+     * Add to a value at a given index in the array
+     * @param index The index of value to add to
+     * @param valueToAdd The amount to add to the value at the given index
+     * @throws ArrayIndexOutOfBoundsException (may throw) if value is exceeds length()
+     */
+    public void add(final int index, final long valueToAdd) throws ArrayIndexOutOfBoundsException {
+        long criticalValueAtEnter = recordingPhaser.writerCriticalSectionEnter();
+        try {
+            activeArray.add(index, valueToAdd);
+        } finally {
+            recordingPhaser.writerCriticalSectionExit(criticalValueAtEnter);
+        }
+    }
+
+    /**
+     * Get an interval array, which will include a stable, consistent view of all values
+     * accumulated since the last interval array was taken.
+     * <p>
+     * Calling this method is equivalent to calling {@code getIntervalArray(null)}. It is generally recommended
+     * that the {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalHistogram(arrayToRecycle)} orm be used for
+     * regular interval array sampling, as that form accepts a previously returned interval array that can be
+     * recycled internally to avoid allocation and content copying operations, and is therefore significantly
+     * more efficient for repeated use than {@link PackedArrayRecorder#getIntervalArray()}.
+     * <p>
+     * Calling {@link PackedArrayRecorder#getIntervalArray()} will reset the values at
+     * all indexes of the array tracked by the recorder, and start accumulating values for the next interval.
+     *
+     * @return an array containing the values accumulated since the last interval array was taken.
+     */
+    public synchronized PackedLongArray getIntervalArray() {
+        return getIntervalArray(null);
+    }
+
+    /**
+     * Get an interval array, which will include a stable, consistent view of all values
+     * accumulated since the last interval array was taken.
+     * <p>
+     * {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)}
+     * accepts a previously returned interval array that can be recycled internally to avoid allocation
+     * and content copying operations, and is therefore significantly more efficient for repeated use than
+     * {@link PackedArrayRecorder#getIntervalArray()}. The provided {@code arrayToRecycle} must
+     * be either be null or an interval array returned by a previous call to
+     * {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)} or
+     * {@link PackedArrayRecorder#getIntervalArray()}.
+     * <p>
+     * NOTE: The caller is responsible for not recycling the same returned interval array more than once. If
+     * the same interval array instance is recycled more than once, behavior is undefined.
+     * <p>
+     * Calling {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)} will reset the values at all indexes of the array
+     * tracked by the recorder, and start accumulating values for the next interval.
+     *
+     * @param arrayToRecycle a previously returned interval array (from this instance of
+     *                           {@link PackedArrayRecorder}) that may be recycled to avoid allocation and
+     *                           copy operations.
+     * @return an array containing the values accumulated since the last interval array was taken.
+     */
+    public synchronized PackedLongArray getIntervalArray(final PackedLongArray arrayToRecycle) {
+        return getIntervalArray(arrayToRecycle, true);
+    }
+
+    /**
+     * Get an interval array, which will include a stable, consistent view of all values
+     * accumulated since the last interval array was taken.
+     * <p>
+     * {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)}
+     * accepts a previously returned interval array that can be recycled internally to avoid allocation
+     * and content copying operations, and is therefore significantly more efficient for repeated use than
+     * {@link PackedArrayRecorder#getIntervalArray()}. The provided {@code arrayToRecycle} must
+     * be either be null or an interval array returned by a previous call to
+     * {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)} or
+     * {@link PackedArrayRecorder#getIntervalArray()}.
+     * <p>
+     * NOTE: The caller is responsible for not recycling the same returned interval array more than once. If
+     * the same interval array instance is recycled more than once, behavior is undefined.
+     * <p>
+     * Calling {@link PackedArrayRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle, enforeContainingInstance)} will reset the values at all indexes
+     * of the array tracked by the recorder, and start accumulating values for the next interval.
+     *
+     * @param arrayToRecycle a previously returned interval array that may be recycled to avoid allocation and
+     *                           copy operations.
+     * @param enforeContainingInstance if true, will only allow recycling of arrays previously returned from this
+     *                                 instance of {@link PackedArrayRecorder}. If false, will allow recycling arrays
+     *                                 previously returned by other instances of {@link PackedArrayRecorder}.
+     * @return an array containing the values accumulated since the last interval array was taken.
+     */
+    public synchronized PackedLongArray getIntervalArray(final PackedLongArray arrayToRecycle,
+                                                         final boolean enforeContainingInstance) {
+        // Verify that replacement array can validly be used as an inactive array replacement:
+        validateFitAsReplacementArray(arrayToRecycle, enforeContainingInstance);
+        PackedLongArray sampledArray = performIntervalSample(arrayToRecycle);
+        return sampledArray;
+    }
+
+    /**
+     * Reset the array contents to all zeros.
+     */
+    public synchronized void reset() {
+        // the currently active array is reset each time we flip:
+        performIntervalSample(null);
+    }
+
+    private PackedLongArray performIntervalSample(final PackedLongArray arrayToRecycle) {
+        PackedLongArray inactiveArray = arrayToRecycle;
+        try {
+            recordingPhaser.readerLock();
+
+            // Make sure we have an inactive version to flip in:
+            if (inactiveArray == null) {
+                if (activeArray instanceof InternalConcurrentPackedLongArray) {
+                    inactiveArray = new InternalConcurrentPackedLongArray(instanceId, activeArray.length());
+                } else {
+                    throw new IllegalStateException("Unexpected internal array type for activeArray");
+                }
+            } else {
+                inactiveArray.clear();
+            }
+
+            // Swap active and inactive arrays:
+            final PackedLongArray tempArray = inactiveArray;
+            inactiveArray = activeArray;
+            activeArray = tempArray;
+
+            // Mark end time of previous interval and start time of new one:
+            long now = System.currentTimeMillis();
+            activeArray.setStartTimeStamp(now);
+            inactiveArray.setEndTimeStamp(now);
+
+            // Make sure we are not in the middle of recording a value on the previously active array:
+
+            // Flip phase to make sure no recordings that were in flight pre-flip are still active:
+            recordingPhaser.flipPhase(500000L /* yield in 0.5 msec units if needed */);
+        } finally {
+            recordingPhaser.readerUnlock();
+        }
+        return inactiveArray;
+    }
+
+    private static class InternalConcurrentPackedLongArray extends ConcurrentPackedLongArray {
+        private final long containingInstanceId;
+
+        private InternalConcurrentPackedLongArray(final long id, int virtualLength, final int initialPhysicalLength) {
+            super(virtualLength, initialPhysicalLength);
+            this.containingInstanceId = id;
+        }
+
+        private InternalConcurrentPackedLongArray(final long id, final int virtualLength) {
+            super(virtualLength);
+            this.containingInstanceId = id;
+        }
+    }
+
+    private void validateFitAsReplacementArray(final PackedLongArray replacementArray,
+                                               final boolean enforeContainingInstance) {
+        boolean bad = true;
+        if (replacementArray == null) {
+            bad = false;
+        } else if (replacementArray instanceof InternalConcurrentPackedLongArray) {
+            if ((activeArray instanceof InternalConcurrentPackedLongArray)
+                    &&
+                    ((!enforeContainingInstance) ||
+                            (((InternalConcurrentPackedLongArray)replacementArray).containingInstanceId ==
+                                    ((InternalConcurrentPackedLongArray) activeArray).containingInstanceId)
+                    )) {
+                bad = false;
+            }
+        }
+        if (bad) {
+            throw new IllegalArgumentException("replacement array must have been obtained via a previous" +
+                    " getIntervalArray() call from this " + this.getClass().getName() +
+                    (enforeContainingInstance ? " insatnce" : " class"));
+        }
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/PackedArraySingleWriterRecorder.java b/src/main/java/org/HdrHistogram/packedarray/PackedArraySingleWriterRecorder.java
new file mode 100644
index 0000000..3c2faa7
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/PackedArraySingleWriterRecorder.java
@@ -0,0 +1,298 @@
+/**
+ * Written by Gil Tene of Azul Systems, and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * @author Gil Tene
+ */
+
+package org.HdrHistogram.packedarray;
+
+import org.HdrHistogram.SingleWriterRecorder;
+import org.HdrHistogram.WriterReaderPhaser;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Records increments and adds of integer values at indexes of a logical array of 64 bit signed integer values, and
+ * provides stable interval {@link PackedLongArray} samples from live recorded data without interrupting or stalling
+ * active recording of values. Each interval array provided contains all values accumulated since the previous
+ * interval array was taken.
+ * <p>
+ * This pattern is commonly used in logging interval accumulator information while recording is ongoing.
+ * <p>
+ * {@link PackedArraySingleWriterRecorder} expects only a single thread (the "single writer") to call
+ * {@link PackedArraySingleWriterRecorder#increment(int)} or
+ * {@link PackedArraySingleWriterRecorder#add(int, long)} at any point in time.
+ * It DOES NOT safely support concurrent increment or add calls.
+ * While the {@link #increment increment()} and {@link #add add()} methods are not quite wait-free, they
+ * come "close" to that behvaior in the sense that a given thread will incur a total of no more than a capped
+ * fixed number (e.g. 74 in a current implementation) of non-wait-free add or increment operations during
+ * the lifetime of an interval array (including across recycling of that array across intervals within the
+ * same recorder), regradless of the number of operations done.
+ * <p>
+ * A common pattern for using a {@link PackedArraySingleWriterRecorder} looks like this:
+ * <br><pre><code>
+ * PackedArraySingleWriterRecorder recorder = new PackedArraySingleWriterRecorder(); //
+ * PackedLongArray intervalArray = null;
+ * ...
+ * [start of some loop construct that periodically wants to grab an interval array]
+ *   ...
+ *   // Get interval array, recycling previous interval array:
+ *   intervalArray = recorder.getIntervalArray(intervalArray);
+ *   // Process the interval array, which is nice and stable here:
+ *   myLogWriter.logArrayContents(intervalArray);
+ *   ...
+ * [end of loop construct]
+ * </code></pre>
+ *
+ */
+
+public class PackedArraySingleWriterRecorder {
+    private static AtomicLong instanceIdSequencer = new AtomicLong(1);
+    private final long instanceId = instanceIdSequencer.getAndIncrement();
+
+    private final WriterReaderPhaser recordingPhaser = new WriterReaderPhaser();
+
+    private volatile PackedLongArray activeArray;
+
+    /**
+     * Construct a {@link PackedArraySingleWriterRecorder} with a given (virtual) array length.
+     *
+     * @param virtualLength The (virtual) array length
+     */
+    public PackedArraySingleWriterRecorder(final int virtualLength) {
+        activeArray = new InternalPackedLongArray(instanceId, virtualLength);
+        activeArray.setStartTimeStamp(System.currentTimeMillis());
+    }
+
+    /**
+     * Construct a {@link PackedArraySingleWriterRecorder} with a given (virtual) array length, starting with a given
+     * initial physical backing store length
+     *
+     * @param virtualLength The (virtual) array length
+     * @param initialPhysicalLength The initial physical backing store length
+     */
+    public PackedArraySingleWriterRecorder(final int virtualLength, final int initialPhysicalLength) {
+        activeArray = new InternalPackedLongArray(instanceId, virtualLength, initialPhysicalLength);
+        activeArray.setStartTimeStamp(System.currentTimeMillis());
+    }
+
+    /**
+     * Returns the virtual length of the array represented by this recorder
+     * @return The virtual length of the array represented by this recorder
+     */
+    public int length() {
+        return activeArray.length();
+    }
+
+    /**
+     * Change the (virtual) length of the array represented by the this recorder
+     * @param newVirtualLength the new (virtual) length to use
+     */
+    public void setVirtualLength(int newVirtualLength) {
+        try {
+            recordingPhaser.readerLock();
+            // We don't care about concurrent modifications to the array, as setVirtualLength() in the
+            // ConcurrentPackedLongArray takes care of those. However, we must perform the change of virtual
+            // length under the recorder's readerLock proptection to prevent mid-change observations:
+            activeArray.setVirtualLength(newVirtualLength);
+        } finally {
+            recordingPhaser.readerUnlock();
+        }
+    }
+
+    /**
+     * Increment a value at a given index in the array
+     * @param index the index of trhe value to be incremented
+     * @throws ArrayIndexOutOfBoundsException (may throw) if value is exceeds length()
+     */
+    public void increment(final int index) throws ArrayIndexOutOfBoundsException {
+        long criticalValueAtEnter = recordingPhaser.writerCriticalSectionEnter();
+        try {
+            activeArray.increment(index);
+        } finally {
+            recordingPhaser.writerCriticalSectionExit(criticalValueAtEnter);
+        }
+    }
+
+    /**
+     * Add to a value at a given index in the array
+     * @param index The index of value to add to
+     * @param valueToAdd The amount to add to the value at the given index
+     * @throws ArrayIndexOutOfBoundsException (may throw) if value is exceeds length()
+     */
+    public void add(final int index, final long valueToAdd) throws ArrayIndexOutOfBoundsException {
+        long criticalValueAtEnter = recordingPhaser.writerCriticalSectionEnter();
+        try {
+            activeArray.add(index, valueToAdd);
+        } finally {
+            recordingPhaser.writerCriticalSectionExit(criticalValueAtEnter);
+        }
+    }
+
+    /**
+     * Get an interval array, which will include a stable, consistent view of all values
+     * accumulated since the last interval array was taken.
+     * <p>
+     * Calling this method is equivalent to calling {@code getIntervalArray(null)}. It is generally recommended
+     * that the {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalHistogram(arrayToRecycle)} orm be used for
+     * regular interval array sampling, as that form accepts a previously returned interval array that can be
+     * recycled internally to avoid allocation and content copying operations, and is therefore significantly
+     * more efficient for repeated use than {@link PackedArraySingleWriterRecorder#getIntervalArray()}.
+     * <p>
+     * Calling {@link PackedArraySingleWriterRecorder#getIntervalArray()} will reset the values at
+     * all indexes of the array tracked by the recorder, and start accumulating values for the next interval.
+     *
+     * @return an array containing the values accumulated since the last interval array was taken.
+     */
+    public synchronized PackedLongArray getIntervalArray() {
+        return getIntervalArray(null);
+    }
+
+    /**
+     * Get an interval array, which will include a stable, consistent view of all values
+     * accumulated since the last interval array was taken.
+     * <p>
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)}
+     * accepts a previously returned interval array that can be recycled internally to avoid allocation
+     * and content copying operations, and is therefore significantly more efficient for repeated use than
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray()}. The provided {@code arrayToRecycle} must
+     * be either be null or an interval array returned by a previous call to
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)} or
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray()}.
+     * <p>
+     * NOTE: The caller is responsible for not recycling the same returned interval array more than once. If
+     * the same interval array instance is recycled more than once, behavior is undefined.
+     * <p>
+     * Calling {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)} will reset the values at all indexes of the array
+     * tracked by the recorder, and start accumulating values for the next interval.
+     *
+     * @param arrayToRecycle a previously returned interval array (from this instance of
+     *                           {@link PackedArraySingleWriterRecorder}) that may be recycled to avoid allocation and
+     *                           copy operations.
+     * @return an array containing the values accumulated since the last interval array was taken.
+     */
+    public synchronized PackedLongArray getIntervalArray(final PackedLongArray arrayToRecycle) {
+        return getIntervalArray(arrayToRecycle, true);
+    }
+
+    /**
+     * Get an interval array, which will include a stable, consistent view of all values
+     * accumulated since the last interval array was taken.
+     * <p>
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)}
+     * accepts a previously returned interval array that can be recycled internally to avoid allocation
+     * and content copying operations, and is therefore significantly more efficient for repeated use than
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray()}. The provided {@code arrayToRecycle} must
+     * be either be null or an interval array returned by a previous call to
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle)} or
+     * {@link PackedArraySingleWriterRecorder#getIntervalArray()}.
+     * <p>
+     * NOTE: The caller is responsible for not recycling the same returned interval array more than once. If
+     * the same interval array instance is recycled more than once, behavior is undefined.
+     * <p>
+     * Calling {@link PackedArraySingleWriterRecorder#getIntervalArray(PackedLongArray arrayToRecycle)
+     * getIntervalArray(arrayToRecycle, enforeContainingInstance)} will reset the values at all indexes
+     * of the array tracked by the recorder, and start accumulating values for the next interval.
+     *
+     * @param arrayToRecycle a previously returned interval array that may be recycled to avoid allocation and
+     *                           copy operations.
+     * @param enforeContainingInstance if true, will only allow recycling of arrays previously returned from this
+     *                                 instance of {@link PackedArraySingleWriterRecorder}. If false, will allow recycling arrays
+     *                                 previously returned by other instances of {@link PackedArraySingleWriterRecorder}.
+     * @return an array containing the values accumulated since the last interval array was taken.
+     */
+    public synchronized PackedLongArray getIntervalArray(final PackedLongArray arrayToRecycle,
+                                                         final boolean enforeContainingInstance) {
+        // Verify that replacement array can validly be used as an inactive array replacement:
+        validateFitAsReplacementArray(arrayToRecycle, enforeContainingInstance);
+        PackedLongArray sampledArray = performIntervalSample(arrayToRecycle);
+        return sampledArray;
+    }
+
+    /**
+     * Reset the array contents to all zeros.
+     */
+    public synchronized void reset() {
+        // the currently active array is reset each time we flip:
+        performIntervalSample(null);
+    }
+
+    private PackedLongArray performIntervalSample(final PackedLongArray arrayToRecycle) {
+        PackedLongArray inactiveArray = arrayToRecycle;
+        try {
+            recordingPhaser.readerLock();
+
+            // Make sure we have an inactive version to flip in:
+            if (inactiveArray == null) {
+                if (activeArray instanceof InternalPackedLongArray) {
+                    inactiveArray = new InternalPackedLongArray(instanceId, activeArray.length());
+                } else {
+                    throw new IllegalStateException("Unexpected internal array type for activeArray");
+                }
+            } else {
+                inactiveArray.clear();
+            }
+
+            // Swap active and inactive arrays:
+            final PackedLongArray tempArray = inactiveArray;
+            inactiveArray = activeArray;
+            activeArray = tempArray;
+
+            // Mark end time of previous interval and start time of new one:
+            long now = System.currentTimeMillis();
+            activeArray.setStartTimeStamp(now);
+            inactiveArray.setEndTimeStamp(now);
+
+            // Make sure we are not in the middle of recording a value on the previously active array:
+
+            // Flip phase to make sure no recordings that were in flight pre-flip are still active:
+            recordingPhaser.flipPhase(500000L /* yield in 0.5 msec units if needed */);
+        } finally {
+            recordingPhaser.readerUnlock();
+        }
+        return inactiveArray;
+    }
+
+    private static class InternalPackedLongArray extends PackedLongArray {
+        private final long containingInstanceId;
+
+        private InternalPackedLongArray(final long id, int virtualLength, final int initialPhysicalLength) {
+            super(virtualLength, initialPhysicalLength);
+            this.containingInstanceId = id;
+        }
+
+        private InternalPackedLongArray(final long id, final int virtualLength) {
+            super(virtualLength);
+            this.containingInstanceId = id;
+        }
+    }
+
+    private void validateFitAsReplacementArray(final PackedLongArray replacementArray,
+                                               final boolean enforeContainingInstance) {
+        boolean bad = true;
+        if (replacementArray == null) {
+            bad = false;
+        } else if (replacementArray instanceof InternalPackedLongArray) {
+            if ((activeArray instanceof InternalPackedLongArray)
+                    &&
+                    ((!enforeContainingInstance) ||
+                            (((InternalPackedLongArray)replacementArray).containingInstanceId ==
+                                    ((InternalPackedLongArray) activeArray).containingInstanceId)
+                    )) {
+                bad = false;
+            }
+        }
+        if (bad) {
+            throw new IllegalArgumentException("replacement array must have been obtained via a previous" +
+                    " getIntervalArray() call from this " + this.getClass().getName() +
+                    (enforeContainingInstance ? " insatnce" : " class"));
+        }
+    }
+}
diff --git a/src/main/java/org/HdrHistogram/packedarray/PackedLongArray.java b/src/main/java/org/HdrHistogram/packedarray/PackedLongArray.java
new file mode 100644
index 0000000..e23bda5
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/PackedLongArray.java
@@ -0,0 +1,73 @@
+package org.HdrHistogram.packedarray;
+
+/**
+ * A Packed array of signed 64 bit values, and supports {@link #get get()}, {@link #set set()},
+ * {@link #add add()} and {@link #increment increment()} operations on the logical contents of the array.
+ */
+public class PackedLongArray extends AbstractPackedLongArray {
+
+    PackedLongArray() {}
+
+    public PackedLongArray(final int virtualLength) {
+        this(virtualLength, AbstractPackedArrayContext.MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY);
+    }
+
+    public PackedLongArray(final int virtualLength, final int initialPhysicalLength) {
+        setArrayContext(new PackedArrayContext(virtualLength, initialPhysicalLength));
+    }
+
+    @Override
+    void resizeStorageArray(final int newPhysicalLengthInLongs) {
+        AbstractPackedArrayContext oldArrayContext = getArrayContext();
+        PackedArrayContext newArrayContext =
+                new PackedArrayContext(oldArrayContext.getVirtualLength(), oldArrayContext, newPhysicalLengthInLongs);
+        setArrayContext(newArrayContext);
+        for (IterationValue v : oldArrayContext.nonZeroValues()) {
+            set(v.getIndex(), v.getValue());
+        }
+    }
+
+    @Override
+    public void setVirtualLength(final int newVirtualArrayLength) {
+        if (newVirtualArrayLength < length()) {
+            throw new IllegalArgumentException(
+                    "Cannot set virtual length, as requested length " + newVirtualArrayLength +
+                            " is smaller than the current virtual length " + length());
+        }
+        AbstractPackedArrayContext currentArrayContext = getArrayContext();
+        if (currentArrayContext.isPacked() &&
+                (currentArrayContext.determineTopLevelShiftForVirtualLength(newVirtualArrayLength) ==
+                        currentArrayContext.getTopLevelShift())) {
+            // No changes to the array context contents is needed. Just change the virtual length.
+            currentArrayContext.setVirtualLength(newVirtualArrayLength);
+            return;
+        }
+        AbstractPackedArrayContext oldArrayContext = currentArrayContext;
+        setArrayContext(new PackedArrayContext(newVirtualArrayLength, oldArrayContext, oldArrayContext.length()));
+        for (IterationValue v : oldArrayContext.nonZeroValues()) {
+            set(v.getIndex(), v.getValue());
+        }
+    }
+
+    @Override
+    public PackedLongArray copy() {
+        PackedLongArray copy = new PackedLongArray(this.length(), this.getPhysicalLength());
+        copy.add(this);
+        return copy;
+    }
+
+    @Override
+    void clearContents() {
+        getArrayContext().clearContents();
+    }
+
+    @Override
+    long criticalSectionEnter() {
+        return 0;
+    }
+
+    @Override
+    void criticalSectionExit(final long criticalValueAtEnter) {
+    }
+}
+
diff --git a/src/main/java/org/HdrHistogram/packedarray/ResizeException.java b/src/main/java/org/HdrHistogram/packedarray/ResizeException.java
new file mode 100644
index 0000000..c91d010
--- /dev/null
+++ b/src/main/java/org/HdrHistogram/packedarray/ResizeException.java
@@ -0,0 +1,13 @@
+package org.HdrHistogram.packedarray;
+
+class ResizeException extends Exception {
+    private int newSize;
+
+    ResizeException(final int newSize) {
+        this.newSize = newSize;
+    }
+
+    int getNewSize() {
+        return newSize;
+    }
+}
diff --git a/src/perf/java/org/HdrHistogram/HistogramPerfTest.java b/src/perf/java/org/HdrHistogram/HistogramPerfTest.java
index 5465581..bd93718 100644
--- a/src/perf/java/org/HdrHistogram/HistogramPerfTest.java
+++ b/src/perf/java/org/HdrHistogram/HistogramPerfTest.java
@@ -8,7 +8,7 @@
 
 package org.HdrHistogram;
 
-import org.junit.*;
+import org.junit.jupiter.api.Test;
 
 /**
  * JUnit test for {@link org.HdrHistogram.Histogram}
@@ -308,6 +308,22 @@ public class HistogramPerfTest {
         testRawRecordingSpeedAtExpectedInterval("Histogram: ", histogram, 1000000000, rawTimingLoopCount);
     }
 
+    @Test
+    public void testRawPackedRecordingSpeedSingleValue() throws Exception {
+        AbstractHistogram histogram;
+        histogram = new PackedHistogram(highestTrackableValue, numberOfSignificantValueDigits);
+        System.out.println("\n\nTiming PackedHistogram:");
+        testRawRecordingSpeedSingleValue("PackedHistogram: ", histogram, rawTimingLoopCount);
+    }
+
+    @Test
+    public void testRawPackedRecordingSpeed() throws Exception {
+        AbstractHistogram histogram;
+        histogram = new PackedHistogram(highestTrackableValue, numberOfSignificantValueDigits);
+        System.out.println("\n\nTiming PackedHistogram:");
+        testRawRecordingSpeedAtExpectedInterval("PackedHistogram: ", histogram, 1000000000, rawTimingLoopCount);
+    }
+
     @Test
     public void testSingleWriterIntervalRecordingSpeed() throws Exception {
         SingleWriterRecorder histogramRecorder;
diff --git a/src/test/java/org/HdrHistogram/ConcurrentHistogramTest.java b/src/test/java/org/HdrHistogram/ConcurrentHistogramTest.java
index ce9feac..9583868 100644
--- a/src/test/java/org/HdrHistogram/ConcurrentHistogramTest.java
+++ b/src/test/java/org/HdrHistogram/ConcurrentHistogramTest.java
@@ -9,7 +9,9 @@
 package org.HdrHistogram;
 
 import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.util.Random;
 import java.util.concurrent.Semaphore;
@@ -25,12 +27,8 @@ public class ConcurrentHistogramTest {
 
     @Test
     public void testConcurrentAutoSizedRecording() throws Exception {
-        doConcurrentRecordValues();
-    }
-
-    public void doConcurrentRecordValues() throws Exception {
         ConcurrentHistogram histogram = new ConcurrentHistogram(2);
-        ValueRecorder valueRecorders[] = new ValueRecorder[24];
+        ValueRecorder valueRecorders[] = new ValueRecorder[64];
         doRun = true;
         waitToGo = true;
         for (int i = 0; i < valueRecorders.length; i++) {
@@ -40,7 +38,7 @@ public class ConcurrentHistogramTest {
 
         long sumOfCounts;
 
-        for (int i = 0; i < 500; i++) {
+        for (int i = 0; i < 1000; i++) {
 
             // Ready:
             sumOfCounts = 0;
@@ -62,7 +60,7 @@ public class ConcurrentHistogramTest {
                 v.setSem.release();
             }
 
-            Thread.sleep(1);
+            Thread.sleep(2);
 
             // Go! :
             waitToGo = false;
diff --git a/src/test/java/org/HdrHistogram/DoubleHistogramDataAccessTest.java b/src/test/java/org/HdrHistogram/DoubleHistogramDataAccessTest.java
index 89c469e..6ab7ae1 100644
--- a/src/test/java/org/HdrHistogram/DoubleHistogramDataAccessTest.java
+++ b/src/test/java/org/HdrHistogram/DoubleHistogramDataAccessTest.java
@@ -9,7 +9,7 @@
 package org.HdrHistogram;
 
 import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 /**
  * JUnit test for {@link Histogram}
diff --git a/src/test/java/org/HdrHistogram/DoubleHistogramTest.java b/src/test/java/org/HdrHistogram/DoubleHistogramTest.java
index 0ec1469..15eead9 100644
--- a/src/test/java/org/HdrHistogram/DoubleHistogramTest.java
+++ b/src/test/java/org/HdrHistogram/DoubleHistogramTest.java
@@ -8,14 +8,21 @@
 
 package org.HdrHistogram;
 
+import static org.HdrHistogram.HistogramTestUtils.constructHistogram;
 import static org.junit.Assert.*;
 
 import org.junit.Assert;
 import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.io.*;
 import java.util.zip.Deflater;
 
+import static org.HdrHistogram.HistogramTestUtils.constructDoubleHistogram;
+
 /**
  * JUnit test for {@link Histogram}
  */
@@ -25,27 +32,77 @@ public class DoubleHistogramTest {
     // static final long testValueLevel = 12340;
     static final double testValueLevel = 4.0;
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testTrackableValueRangeMustBeGreaterThanTwo() throws Exception
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testTrackableValueRangeMustBeGreaterThanTwo(final Class histoClass) throws Exception
     {
-        new DoubleHistogram(1, numberOfSignificantValueDigits);
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleHistogram histogram =
+                                constructDoubleHistogram(histoClass, 1, numberOfSignificantValueDigits);
+                    }
+                });
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testNumberOfSignificantValueDigitsMustBeLessThanSix() throws Exception
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testNumberOfSignificantValueDigitsMustBeLessThanSix(final Class histoClass) throws Exception
     {
-        new DoubleHistogram(trackableValueRangeSize, 6);
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleHistogram histogram =
+                                constructDoubleHistogram(histoClass, trackableValueRangeSize, 6);
+                    }
+                });
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testNumberOfSignificantValueDigitsMustBePositive() throws Exception
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testNumberOfSignificantValueDigitsMustBePositive(final Class histoClass) throws Exception
     {
-        new DoubleHistogram(trackableValueRangeSize, -1);
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleHistogram histogram =
+                                constructDoubleHistogram(histoClass, trackableValueRangeSize, -1);
+                    }
+                });
     }
 
-    @Test
-    public void testConstructionArgumentGets() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testConstructionArgumentGets(Class histoClass) throws Exception {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         // Record 1.0, and verify that the range adjust to it:
         histogram.recordValue(Math.pow(2.0, 20));
         histogram.recordValue(1.0);
@@ -53,21 +110,31 @@ public class DoubleHistogramTest {
         assertEquals(trackableValueRangeSize, histogram.getHighestToLowestValueRatio());
         assertEquals(numberOfSignificantValueDigits, histogram.getNumberOfSignificantValueDigits());
 
-        DoubleHistogram histogram2 = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+        DoubleHistogram histogram2 =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         // Record a larger value, and verify that the range adjust to it too:
         histogram2.recordValue(2048.0 * 1024.0 * 1024.0);
         assertEquals(2048.0 * 1024.0 * 1024.0, histogram2.getCurrentLowestTrackableNonZeroValue(), 0.001);
 
-        DoubleHistogram histogram3 = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+        DoubleHistogram histogram3 =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         // Record a value that is 1000x outside of the initially set range, which should scale us by 1/1024x:
         histogram3.recordValue(1/1000.0);
         assertEquals(1.0/1024, histogram3.getCurrentLowestTrackableNonZeroValue(), 0.001);
     }
 
-    @Test
-    public void testDataRange() {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testDataRange(Class histoClass) {
         // A trackableValueRangeSize histigram
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(0.0);  // Include a zero value to make sure things are handled right.
         assertEquals(1L, histogram.getCountAtValue(0.0));
 
@@ -82,7 +149,7 @@ public class DoubleHistogramTest {
         assertEquals(1L << 33, topValue, 0.00001);
         assertEquals(1L, histogram.getCountAtValue(0.0));
 
-        histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+        histogram = constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(0.0); // Include a zero value to make sure things are handled right.
 
         double bottomValue = 1L << 33;
@@ -101,27 +168,58 @@ public class DoubleHistogramTest {
         assertEquals(1L, histogram.getCountAtValue(0.0));
     }
 
-    @Test
-    public void testRecordValue() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testRecordValue(Class histoClass) throws Exception {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         assertEquals(1L, histogram.getCountAtValue(testValueLevel));
         assertEquals(1L, histogram.getTotalCount());
     }
 
-    @Test(expected = ArrayIndexOutOfBoundsException.class)
-    public void testRecordValue_Overflow_ShouldThrowException() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
-        histogram.recordValue(trackableValueRangeSize * 3);
-        histogram.recordValue(1.0);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testRecordValue_Overflow_ShouldThrowException(final Class histoClass) throws Exception {
+        Assertions.assertThrows(ArrayIndexOutOfBoundsException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleHistogram histogram =
+                                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
+                        histogram.recordValue(trackableValueRangeSize * 3);
+                        histogram.recordValue(1.0);
+                    }
+                });
     }
 
-    @Test
-    public void testRecordValueWithExpectedInterval() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testRecordValueWithExpectedInterval(Class histoClass) throws Exception {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(0);
         histogram.recordValueWithExpectedInterval(testValueLevel, testValueLevel/4);
-        DoubleHistogram rawHistogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+        DoubleHistogram rawHistogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         rawHistogram.recordValue(0);
         rawHistogram.recordValue(testValueLevel);
         // The raw data will not include corrected samples:
@@ -140,9 +238,17 @@ public class DoubleHistogramTest {
         assertEquals(5L, histogram.getTotalCount());
     }
 
-    @Test
-    public void testReset() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testReset(final Class histoClass) throws Exception {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         histogram.recordValue(10);
         histogram.recordValue(100);
@@ -157,10 +263,19 @@ public class DoubleHistogramTest {
         Assert.assertEquals(histogram.getMaxValue(), 80.0, 1.0);
     }
 
-    @Test
-    public void testAdd() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
-        DoubleHistogram other = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testAdd(final Class histoClass) throws Exception {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
+        DoubleHistogram other =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
 
         histogram.recordValue(testValueLevel);
         histogram.recordValue(testValueLevel * 1000);
@@ -171,7 +286,8 @@ public class DoubleHistogramTest {
         assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
         assertEquals(4L, histogram.getTotalCount());
 
-        DoubleHistogram biggerOther = new DoubleHistogram(trackableValueRangeSize * 2, numberOfSignificantValueDigits);
+        DoubleHistogram biggerOther =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize * 2, numberOfSignificantValueDigits);
         biggerOther.recordValue(testValueLevel);
         biggerOther.recordValue(testValueLevel * 1000);
 
@@ -203,9 +319,16 @@ public class DoubleHistogramTest {
         }
     }
 
-    @Test
-    public void testAddWithAutoResize() {
-        DoubleHistogram histo1 = new DoubleHistogram(3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testAddWithAutoResize(final Class histoClass) {
+        DoubleHistogram histo1 = constructDoubleHistogram(histoClass, 3);
         histo1.setAutoResize(true);
         histo1.recordValue(6.0);
         histo1.recordValue(1.0);
@@ -213,16 +336,16 @@ public class DoubleHistogramTest {
         histo1.recordValue(8.0);
         histo1.recordValue(3.0);
         histo1.recordValue(7.0);
-        DoubleHistogram histo2 = new DoubleHistogram(3);
+        DoubleHistogram histo2 = constructDoubleHistogram(histoClass, 3);
         histo2.setAutoResize(true);
         histo2.recordValue(9.0);
-        DoubleHistogram histo3 = new DoubleHistogram(3);
+        DoubleHistogram histo3 = constructDoubleHistogram(histoClass, 3);
         histo3.setAutoResize(true);
         histo3.recordValue(4.0);
         histo3.recordValue(2.0);
         histo3.recordValue(10.0);
 
-        DoubleHistogram merged = new DoubleHistogram(3);
+        DoubleHistogram merged = constructDoubleHistogram(histoClass, 3);
         merged.setAutoResize(true);
         merged.add(histo1);
         merged.add(histo2);
@@ -234,9 +357,17 @@ public class DoubleHistogramTest {
         assertEquals(10.0, merged.getMaxValue(), 0.01);
     }
 
-    @Test
-    public void testSizeOfEquivalentValueRange() {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testSizeOfEquivalentValueRange(final Class histoClass) {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(1.0);
         assertEquals("Size of equivalent range for value 1 is 1",
                 1.0/1024.0, histogram.sizeOfEquivalentValueRange(1), 0.001);
@@ -250,9 +381,17 @@ public class DoubleHistogramTest {
                 8, histogram.sizeOfEquivalentValueRange(10000), 0.001);
     }
 
-    @Test
-    public void testLowestEquivalentValue() {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testLowestEquivalentValue(final Class histoClass) {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(1.0);
         assertEquals("The lowest equivalent value to 10007 is 10000",
                 10000, histogram.lowestEquivalentValue(10007), 0.001);
@@ -260,9 +399,17 @@ public class DoubleHistogramTest {
                 10008, histogram.lowestEquivalentValue(10009), 0.001);
     }
 
-    @Test
-    public void testHighestEquivalentValue() {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testHighestEquivalentValue(final Class histoClass) {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(1.0);
         assertEquals("The highest equivalent value to 8180 is 8183",
                 8183.99999, histogram.highestEquivalentValue(8180), 0.001);
@@ -278,9 +425,17 @@ public class DoubleHistogramTest {
                 10015.99999, histogram.highestEquivalentValue(10008), 0.001);
     }
 
-    @Test
-    public void testMedianEquivalentValue() {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testMedianEquivalentValue(final Class histoClass) {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(1.0);
         assertEquals("The median equivalent value to 4 is 4",
                 4.002, histogram.medianEquivalentValue(4), 0.001);
@@ -294,9 +449,17 @@ public class DoubleHistogramTest {
                 10004, histogram.medianEquivalentValue(10007), 0.001);
     }
 
-    @Test
-    public void testNextNonEquivalentValue() {
-        DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testNextNonEquivalentValue(final Class histoClass) {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass, trackableValueRangeSize, numberOfSignificantValueDigits);
         assertNotSame(null, histogram);
     }
 
@@ -356,27 +519,53 @@ public class DoubleHistogramTest {
         synchronizedDoubleHistogram.equals(other);
     }
 
-    @Test
-    public void testSerialization() throws Exception {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testSerialization(final Class histoClass) throws Exception {
+        DoubleHistogram histogram =
+                constructDoubleHistogram(histoClass,trackableValueRangeSize, 3);
+        testDoubleHistogramSerialization(histogram);
+        histogram = constructDoubleHistogram(histoClass,trackableValueRangeSize, 2);
+        testDoubleHistogramSerialization(histogram);
+    }
+
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+    })
+    public void testSerializationWithInternals(final Class histoClass) throws Exception {
         DoubleHistogram histogram =
-                new DoubleHistogram(trackableValueRangeSize, 3);
+                constructDoubleHistogram(histoClass,trackableValueRangeSize, 3);
         testDoubleHistogramSerialization(histogram);
         DoubleHistogram withIntHistogram =
-                new DoubleHistogram(trackableValueRangeSize, 3, IntCountsHistogram.class);
+                constructDoubleHistogram(histoClass,trackableValueRangeSize, 3, IntCountsHistogram.class);
         testDoubleHistogramSerialization(withIntHistogram);
         DoubleHistogram withShortHistogram =
-                new DoubleHistogram(trackableValueRangeSize, 3, ShortCountsHistogram.class);
+                constructDoubleHistogram(histoClass,trackableValueRangeSize, 3, ShortCountsHistogram.class);
         testDoubleHistogramSerialization(withShortHistogram);
-        histogram = new DoubleHistogram(trackableValueRangeSize, 2, Histogram.class);
+        histogram = constructDoubleHistogram(histoClass,trackableValueRangeSize, 2, Histogram.class);
         testDoubleHistogramSerialization(histogram);
-        withIntHistogram = new DoubleHistogram(trackableValueRangeSize, 2, IntCountsHistogram.class);
+        withIntHistogram = constructDoubleHistogram(histoClass,trackableValueRangeSize, 2, IntCountsHistogram.class);
         testDoubleHistogramSerialization(withIntHistogram);
-        withShortHistogram = new DoubleHistogram(trackableValueRangeSize, 2, ShortCountsHistogram.class);
+        withShortHistogram = constructDoubleHistogram(histoClass,trackableValueRangeSize, 2, ShortCountsHistogram.class);
         testDoubleHistogramSerialization(withShortHistogram);
     }
 
-    @Test
-    public void testCopy() throws Exception {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testCopy(final Class histoClass) throws Exception {
         DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         histogram.recordValue(testValueLevel * 10);
@@ -422,8 +611,15 @@ public class DoubleHistogramTest {
         assertEqual(withSyncHistogram, withSyncHistogram.copy());
     }
 
-    @Test
-    public void testCopyInto() throws Exception {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testCopyInto(final Class histoClass) throws Exception {
         DoubleHistogram histogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
         DoubleHistogram targetHistogram = new DoubleHistogram(trackableValueRangeSize, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
@@ -565,13 +761,31 @@ public class DoubleHistogramTest {
         Assert.assertEquals(9.0, h.getValueAtPercentile(100), 0.1d);
     }
 
-    @Test
-    public void testResize() {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testResize(final Class histoClass) {
+        // Verify resize behvaior for various underlying internal integer histogram implementations:
+        genericResizeTest(constructDoubleHistogram(histoClass, 2));
+    }
+
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+    })
+    public void testResizeInternals(final Class histoClass) {
         // Verify resize behvaior for various underlying internal integer histogram implementations:
-        genericResizeTest(new DoubleHistogram(2));
-        genericResizeTest(new DoubleHistogram(2, IntCountsHistogram.class));
-        genericResizeTest(new DoubleHistogram(2, ShortCountsHistogram.class));
-        genericResizeTest(new DoubleHistogram(2, ConcurrentHistogram.class));
-        genericResizeTest(new DoubleHistogram(2, SynchronizedHistogram.class));
+        genericResizeTest(constructDoubleHistogram(histoClass, 2));
+        genericResizeTest(constructDoubleHistogram(histoClass,2, IntCountsHistogram.class));
+        genericResizeTest(constructDoubleHistogram(histoClass,2, ShortCountsHistogram.class));
+        genericResizeTest(constructDoubleHistogram(histoClass,2, ConcurrentHistogram.class));
+        genericResizeTest(constructDoubleHistogram(histoClass,2, SynchronizedHistogram.class));
+        genericResizeTest(constructDoubleHistogram(histoClass,2, PackedHistogram.class));
+        genericResizeTest(constructDoubleHistogram(histoClass,2, PackedConcurrentHistogram.class));
     }
 }
diff --git a/src/test/java/org/HdrHistogram/DumpHistogram.java b/src/test/java/org/HdrHistogram/DumpHistogram.java
new file mode 100644
index 0000000..71a92af
--- /dev/null
+++ b/src/test/java/org/HdrHistogram/DumpHistogram.java
@@ -0,0 +1,56 @@
+package org.HdrHistogram;
+
+import java.nio.ByteBuffer;
+
+public class DumpHistogram {
+    static String hist1 = "DHISTwAAAAMAAAAAAAAABByEkxQAAABBeNqTaZkszMDAoMwAAcxQmhFC2f+3OwBhHZdgecrONJWDpZuF5zozSzMTUz8bVzcL03VmjkZGlnqWRkY2AGoTC78=";
+    static String hist2 = "DHISTwAAAAMAAAAAAAAAAhyEkxQAAAAieNqTaZkszMDAwMIAAcxQmhFCyf+32wBhMa0VYAIAUp8EHA==";
+
+    static void dumpHistogram(String histString) throws Exception {
+        final ByteBuffer buffer = ByteBuffer.wrap(Base64Helper.parseBase64Binary(histString));
+        DoubleHistogram histogram = DoubleHistogram.decodeFromCompressedByteBuffer(buffer, 0);
+        dumpHistogram(histogram);
+    }
+
+    static void dumpHistogram(DoubleHistogram histogram) throws Exception {
+        AbstractHistogram iHist = histogram.integerValuesHistogram;
+        System.out.format("digits = %d, min = %12.12g, max = %12.12g\n", histogram.getNumberOfSignificantValueDigits(), histogram.getMinNonZeroValue(), histogram.getMaxValue());
+        System.out.format("lowest = %12.12g, highest = %12.12g\n", histogram.getCurrentLowestTrackableNonZeroValue(), histogram.getCurrentHighestTrackableValue());
+        System.out.format("lowest(i) = %d, highest(i) = %d\n",
+                iHist.countsArrayIndex((long)(histogram.getCurrentLowestTrackableNonZeroValue() * histogram.getDoubleToIntegerValueConversionRatio())),
+                iHist.countsArrayIndex((long)(histogram.getCurrentHighestTrackableValue() * histogram.getDoubleToIntegerValueConversionRatio())));
+        System.out.format("length = %d, imin = %d, imax = %d\n", histogram.integerValuesHistogram.countsArrayLength,
+                histogram.integerValuesHistogram.getMinNonZeroValue(),
+                histogram.integerValuesHistogram.getMaxValue());
+        System.out.format("index 4644 here translates to %12.12g\n", iHist.valueFromIndex(4644) * histogram.getIntegerToDoubleValueConversionRatio());
+        for (DoubleHistogramIterationValue val : histogram.recordedValues()) {
+            HistogramIterationValue iVal = val.getIntegerHistogramIterationValue();
+            int index = iHist.countsArrayIndex(iVal.getValueIteratedTo());
+            System.out.format("[%d] %12.12g, %12.12g - %12.12g : %d\n",index,
+                    val.getValueIteratedTo(),
+                    histogram.lowestEquivalentValue(val.getValueIteratedTo()),
+                    histogram.highestEquivalentValue(val.getValueIteratedTo()),
+                    val.getCountAtValueIteratedTo());
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        System.out.println("hist1:");
+        dumpHistogram(hist1);
+        System.out.println("hist2:");
+        dumpHistogram(hist2);
+
+        DoubleHistogram h = new DoubleHistogram(3);
+        h.recordValue(0.000999451);
+        System.out.println("h:");
+        dumpHistogram(h);
+
+        h.recordValue(h.integerValuesHistogram.valueFromIndex(4644) * h.getIntegerToDoubleValueConversionRatio());
+        System.out.println("h':");
+        dumpHistogram(h);
+
+        h.recordValue(0.0119934);
+        System.out.println("h':");
+        dumpHistogram(h);
+    }
+}
diff --git a/src/test/java/org/HdrHistogram/HistogramAutosizingTest.java b/src/test/java/org/HdrHistogram/HistogramAutosizingTest.java
index c38ec96..36f8370 100644
--- a/src/test/java/org/HdrHistogram/HistogramAutosizingTest.java
+++ b/src/test/java/org/HdrHistogram/HistogramAutosizingTest.java
@@ -9,7 +9,11 @@
 package org.HdrHistogram;
 
 import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.HdrHistogram.HistogramTestUtils.constructHistogram;
+import static org.HdrHistogram.HistogramTestUtils.constructDoubleHistogram;
 
 /**
  * JUnit test for {@link org.HdrHistogram.Histogram}
@@ -17,9 +21,18 @@ import org.junit.Test;
 public class HistogramAutosizingTest {
     static final long highestTrackableValue = 3600L * 1000 * 1000; // e.g. for 1 hr in usec units
 
-    @Test
-    public void testHistogramAutoSizingEdges() throws Exception {
-        Histogram histogram = new Histogram(3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testHistogramAutoSizingEdges(Class c) throws Exception {
+        AbstractHistogram histogram = constructHistogram(c,3);
         histogram.recordValue((1L << 62) - 1);
         Assert.assertEquals(52, histogram.bucketCount);
         Assert.assertEquals(54272, histogram.countsArrayLength);
@@ -28,9 +41,18 @@ public class HistogramAutosizingTest {
         Assert.assertEquals(55296, histogram.countsArrayLength);
     }
 
-    @Test
-    public void testHistogramEqualsAfterResizing() throws Exception {
-        Histogram histogram = new Histogram(3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testHistogramEqualsAfterResizing(Class c) throws Exception {
+        AbstractHistogram histogram = constructHistogram(c,3);
         histogram.recordValue((1L << 62) - 1);
         Assert.assertEquals(52, histogram.bucketCount);
         Assert.assertEquals(54272, histogram.countsArrayLength);
@@ -45,42 +67,18 @@ public class HistogramAutosizingTest {
         Assert.assertEquals(histogram, histogram1);
     }
 
-    @Test
-    public void testDoubleHistogramAutoSizingEdges() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(3);
-        histogram.recordValue(1);
-        histogram.recordValue(1L << 48);
-        histogram.recordValue((1L << 52) - 1);
-        Assert.assertEquals(52, histogram.integerValuesHistogram.bucketCount);
-        Assert.assertEquals(54272, histogram.integerValuesHistogram.countsArrayLength);
-        histogram.recordValue((1L << 53) - 1);
-        Assert.assertEquals(53, histogram.integerValuesHistogram.bucketCount);
-        Assert.assertEquals(55296, histogram.integerValuesHistogram.countsArrayLength);
-
-        DoubleHistogram histogram2 = new DoubleHistogram(2);
-        histogram2.recordValue(1);
-        histogram2.recordValue(1L << 48);
-        histogram2.recordValue((1L << 54) - 1);
-        Assert.assertEquals(55, histogram2.integerValuesHistogram.bucketCount);
-        Assert.assertEquals(7168, histogram2.integerValuesHistogram.countsArrayLength);
-        histogram2.recordValue((1L << 55) - 1);
-        Assert.assertEquals(56, histogram2.integerValuesHistogram.bucketCount);
-        Assert.assertEquals(7296, histogram2.integerValuesHistogram.countsArrayLength);
-
-        DoubleHistogram histogram3 = new DoubleHistogram(2);
-        histogram3.recordValue(1E50);
-        histogram3.recordValue((1L << 48) * 1E50);
-        histogram3.recordValue(((1L << 54) - 1) * 1E50);
-        Assert.assertEquals(55, histogram3.integerValuesHistogram.bucketCount);
-        Assert.assertEquals(7168, histogram3.integerValuesHistogram.countsArrayLength);
-        histogram3.recordValue(((1L << 55) - 1) * 1E50);
-        Assert.assertEquals(56, histogram3.integerValuesHistogram.bucketCount);
-        Assert.assertEquals(7296, histogram3.integerValuesHistogram.countsArrayLength);
-    }
-
-    @Test
-    public void testHistogramAutoSizing() throws Exception {
-        Histogram histogram = new Histogram(3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testHistogramAutoSizing(Class c) throws Exception {
+        AbstractHistogram histogram = constructHistogram(c,3);
         for (int i = 0; i < 63; i++) {
             long value = 1L << i;
             histogram.recordValue(value);
@@ -89,82 +87,56 @@ public class HistogramAutosizingTest {
         Assert.assertEquals(55296, histogram.countsArrayLength);
     }
 
-    @Test
-    public void testConcurrentHistogramAutoSizing() throws Exception {
-        ConcurrentHistogram histogram = new ConcurrentHistogram(3);
-        for (int i = 9; i < 63; i++) {
-            long value = 1L << i;
-            histogram.recordValue(value);
-        }
-    }
-
-    @Test
-    public void testSynchronizedHistogramAutoSizing() throws Exception {
-        SynchronizedHistogram histogram = new SynchronizedHistogram(3);
-        for (int i = 0; i < 63; i++) {
-            long value = 1L << i;
-            histogram.recordValue(value);
-        }
-    }
-
-    @Test
-    public void testIntCountsHistogramAutoSizing() throws Exception {
-        IntCountsHistogram histogram = new IntCountsHistogram(3);
-        for (int i = 0; i < 63; i++) {
-            long value = 1L << i;
-            histogram.recordValue(value);
-        }
-    }
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testAutoSizingAdd(Class c) throws Exception {
+        AbstractHistogram histogram1 = constructHistogram(c, 2);
+        AbstractHistogram histogram2 = constructHistogram(c, 2);
 
-    @Test
-    public void testShortCountsHistogramAutoSizing() throws Exception {
-        ShortCountsHistogram histogram = new ShortCountsHistogram(3);
-        for (int i = 0; i < 63; i++) {
-            long value = 1L << i;
-            histogram.recordValue(value);
-        }
-    }
+        histogram1.recordValue(1000L);
+        histogram1.recordValue(1000000000L);
 
-    @Test
-    public void testDoubleHistogramAutoSizingUp() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(2);
-        for (int i = 0; i < 55; i++) {
-            double value = 1L << i;
-            histogram.recordValue(value);
-        }
-    }
+        histogram2.add(histogram1);
 
-    @Test
-    public void testDoubleHistogramAutoSizingDown() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(2);
-        for (int i = 0; i < 56; i++) {
-            double value = (1L << 45) * 1.0 / (1L << i);
-            histogram.recordValue(value);
-        }
+        Assert.assertTrue("Max should be equivalent to 1000000000L",
+                histogram2.valuesAreEquivalent(histogram2.getMaxValue(), 1000000000L)
+                );
     }
 
-    @Test
-    public void testConcurrentDoubleHistogramAutoSizingDown() throws Exception {
-        ConcurrentDoubleHistogram histogram = new ConcurrentDoubleHistogram(2);
-        for (int i = 0; i < 56; i++) {
-            double value = (1L << 45) * 1.0 / (1L << i);
-            histogram.recordValue(value);
-        }
-    }
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+    })
+    public void testAutoSizingAcrossContinuousRange(Class c) {
+        AbstractHistogram histogram = constructHistogram(c, 2);
 
-    @Test
-    public void testSynchronizedDoubleHistogramAutoSizingDown() throws Exception {
-        SynchronizedDoubleHistogram histogram = new SynchronizedDoubleHistogram(2);
-        for (int i = 0; i < 56; i++) {
-            double value = (1L << 45) * 1.0 / (1L << i);
-            histogram.recordValue(value);
+        for (long i = 0; i < 10000000L; i++) {
+            histogram.recordValue(i);
         }
     }
 
-    @Test
-    public void testAutoSizingAdd() throws Exception {
-        Histogram histogram1 = new Histogram(2);
-        Histogram histogram2 = new Histogram(2);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+    })
+    public void testAutoSizingAddDouble(Class c) throws Exception {
+        DoubleHistogram histogram1 = constructDoubleHistogram(c,2);
+        DoubleHistogram histogram2 = constructDoubleHistogram(c,2);
 
         histogram1.recordValue(1000L);
         histogram1.recordValue(1000000000L);
@@ -173,40 +145,73 @@ public class HistogramAutosizingTest {
 
         Assert.assertTrue("Max should be equivalent to 1000000000L",
                 histogram2.valuesAreEquivalent(histogram2.getMaxValue(), 1000000000L)
-                );
+        );
     }
 
-    @Test
-    public void testAutoSizingAcrossContinuousRange() throws Exception {
-        Histogram histogram = new Histogram(2);
-
-        for (long i = 0; i < 10000000L; i++) {
-            histogram.recordValue(i);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+    })
+    public void testDoubleHistogramAutoSizingUp(Class c) throws Exception {
+        DoubleHistogram histogram = constructDoubleHistogram(c,2);
+        for (int i = 0; i < 55; i++) {
+            double value = 1L << i;
+            histogram.recordValue(value);
         }
     }
 
-    @Test
-    public void testAutoSizingAcrossContinuousRangeConcurrent() throws Exception {
-        Histogram histogram = new ConcurrentHistogram(2);
-
-        for (long i = 0; i < 1000000L; i++) {
-            histogram.recordValue(i);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+    })
+    public void testDoubleHistogramAutoSizingDown(Class c) throws Exception {
+        DoubleHistogram histogram = constructDoubleHistogram(c,2);
+        for (int i = 0; i < 56; i++) {
+            double value = (1L << 45) * 1.0 / (1L << i);
+            histogram.recordValue(value);
         }
     }
 
-    @Test
-    public void testAutoSizingAddDouble() throws Exception {
-        DoubleHistogram histogram1 = new DoubleHistogram(2);
-        DoubleHistogram histogram2 = new DoubleHistogram(2);
-
-        histogram1.recordValue(1000L);
-        histogram1.recordValue(1000000000L);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+    })
+    public void testDoubleHistogramAutoSizingEdges(Class c) throws Exception {
+        DoubleHistogram histogram = constructDoubleHistogram(c,3);;
+        histogram.recordValue(1);
+        histogram.recordValue(1L << 48);
+        histogram.recordValue((1L << 52) - 1);
+        Assert.assertEquals(52, histogram.integerValuesHistogram.bucketCount);
+        Assert.assertEquals(54272, histogram.integerValuesHistogram.countsArrayLength);
+        histogram.recordValue((1L << 53) - 1);
+        Assert.assertEquals(53, histogram.integerValuesHistogram.bucketCount);
+        Assert.assertEquals(55296, histogram.integerValuesHistogram.countsArrayLength);
 
-        histogram2.add(histogram1);
+        DoubleHistogram histogram2 = constructDoubleHistogram(c,2);;
+        histogram2.recordValue(1);
+        histogram2.recordValue(1L << 48);
+        histogram2.recordValue((1L << 54) - 1);
+        Assert.assertEquals(55, histogram2.integerValuesHistogram.bucketCount);
+        Assert.assertEquals(7168, histogram2.integerValuesHistogram.countsArrayLength);
+        histogram2.recordValue((1L << 55) - 1);
+        Assert.assertEquals(56, histogram2.integerValuesHistogram.bucketCount);
+        Assert.assertEquals(7296, histogram2.integerValuesHistogram.countsArrayLength);
 
-        Assert.assertTrue("Max should be equivalent to 1000000000L",
-                histogram2.valuesAreEquivalent(histogram2.getMaxValue(), 1000000000L)
-        );
+        DoubleHistogram histogram3 = constructDoubleHistogram(c,2);;
+        histogram3.recordValue(1E50);
+        histogram3.recordValue((1L << 48) * 1E50);
+        histogram3.recordValue(((1L << 54) - 1) * 1E50);
+        Assert.assertEquals(55, histogram3.integerValuesHistogram.bucketCount);
+        Assert.assertEquals(7168, histogram3.integerValuesHistogram.countsArrayLength);
+        histogram3.recordValue(((1L << 55) - 1) * 1E50);
+        Assert.assertEquals(56, histogram3.integerValuesHistogram.bucketCount);
+        Assert.assertEquals(7296, histogram3.integerValuesHistogram.countsArrayLength);
     }
 
 }
diff --git a/src/test/java/org/HdrHistogram/HistogramDataAccessTest.java b/src/test/java/org/HdrHistogram/HistogramDataAccessTest.java
index 0b84bd1..d4332ec 100644
--- a/src/test/java/org/HdrHistogram/HistogramDataAccessTest.java
+++ b/src/test/java/org/HdrHistogram/HistogramDataAccessTest.java
@@ -9,7 +9,7 @@
 package org.HdrHistogram;
 
 import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/test/java/org/HdrHistogram/HistogramEncodingTest.java b/src/test/java/org/HdrHistogram/HistogramEncodingTest.java
index 18e52a1..4faf098 100644
--- a/src/test/java/org/HdrHistogram/HistogramEncodingTest.java
+++ b/src/test/java/org/HdrHistogram/HistogramEncodingTest.java
@@ -9,15 +9,21 @@
 package org.HdrHistogram;
 
 import org.junit.Assert;
-import org.junit.*;
-import org.junit.experimental.theories.DataPoint;
+import org.junit.jupiter.api.Test;
 import org.junit.experimental.theories.DataPoints;
 import org.junit.experimental.theories.Theories;
 import org.junit.experimental.theories.Theory;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.junit.runner.RunWith;
 
 import java.nio.ByteBuffer;
 
+import static org.HdrHistogram.HistogramTestUtils.constructHistogram;
+import static org.HdrHistogram.HistogramTestUtils.constructDoubleHistogram;
+import static org.HdrHistogram.HistogramTestUtils.decodeFromCompressedByteBuffer;
+import static org.HdrHistogram.HistogramTestUtils.decodeDoubleHistogramFromCompressedByteBuffer;
+
 /**
  * JUnit test for {@link org.HdrHistogram.Histogram}
  */
@@ -65,20 +71,33 @@ public class HistogramEncodingTest {
         ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, 3);
         IntCountsHistogram intCountsHistogram = new IntCountsHistogram(highestTrackableValue, 3);
         Histogram histogram = new Histogram(highestTrackableValue, 3);
+        PackedHistogram packedHistogram = new PackedHistogram(highestTrackableValue, 3);
+        PackedConcurrentHistogram packedConcurrentHistogram = new PackedConcurrentHistogram(highestTrackableValue, 3);
         AtomicHistogram atomicHistogram = new AtomicHistogram(highestTrackableValue, 3);
         ConcurrentHistogram concurrentHistogram = new ConcurrentHistogram(highestTrackableValue, 3);
         SynchronizedHistogram synchronizedHistogram = new SynchronizedHistogram(highestTrackableValue, 3);
         DoubleHistogram doubleHistogram = new DoubleHistogram(highestTrackableValue * 1000, 3);
+        PackedDoubleHistogram packedDoubleHistogram = new PackedDoubleHistogram(highestTrackableValue * 1000, 3);
+        DoubleHistogram concurrentDoubleHistogram = new ConcurrentDoubleHistogram(highestTrackableValue * 1000, 3);
+        PackedConcurrentDoubleHistogram packedConcurrentDoubleHistogram = new PackedConcurrentDoubleHistogram(highestTrackableValue * 1000, 3);
 
         for (int i = 0; i < 10000; i++) {
             shortCountsHistogram.recordValue(1000 * i);
             intCountsHistogram.recordValue(2000 * i);
             histogram.recordValue(3000 * i);
+            packedHistogram.recordValue(3000 * i);
+            packedConcurrentHistogram.recordValue(3000 * i);
             atomicHistogram.recordValue(4000 * i);
             concurrentHistogram.recordValue(4000 * i);
             synchronizedHistogram.recordValue(5000 * i);
             doubleHistogram.recordValue(5000 * i);
             doubleHistogram.recordValue(0.001); // Makes some internal shifts happen.
+            packedDoubleHistogram.recordValue(5000 * i);
+            packedDoubleHistogram.recordValue(0.001); // Makes some internal shifts happen.
+            concurrentDoubleHistogram.recordValue(5000 * i);
+            concurrentDoubleHistogram.recordValue(0.001); // Makes some internal shifts happen.
+            packedConcurrentDoubleHistogram.recordValue(5000 * i);
+            packedConcurrentDoubleHistogram.recordValue(0.001); // Makes some internal shifts happen.
         }
 
         System.out.println("Testing encoding of a ShortHistogram:");
@@ -126,6 +145,36 @@ public class HistogramEncodingTest {
         Histogram histogram3 = Histogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
         Assert.assertEquals(histogram, histogram3);
 
+        System.out.println("Testing encoding of a PackedHistogram:");
+        targetBuffer = allocator.allocate(packedHistogram.getNeededByteBufferCapacity());
+        packedHistogram.encodeIntoByteBuffer(targetBuffer);
+        targetBuffer.rewind();
+
+        PackedHistogram packedHistogram2 = PackedHistogram.decodeFromByteBuffer(targetBuffer, 0);
+        Assert.assertEquals(packedHistogram, packedHistogram2);
+
+        targetCompressedBuffer = allocator.allocate(packedHistogram.getNeededByteBufferCapacity());
+        packedHistogram.encodeIntoCompressedByteBuffer(targetCompressedBuffer);
+        targetCompressedBuffer.rewind();
+
+        PackedHistogram packedHistogram3 = PackedHistogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
+        Assert.assertEquals(packedHistogram, packedHistogram3);
+
+        System.out.println("Testing encoding of a PackedConcurrentHistogram:");
+        targetBuffer = allocator.allocate(packedConcurrentHistogram.getNeededByteBufferCapacity());
+        packedConcurrentHistogram.encodeIntoByteBuffer(targetBuffer);
+        targetBuffer.rewind();
+
+        PackedConcurrentHistogram packedConcurrentHistogram2 = PackedConcurrentHistogram.decodeFromByteBuffer(targetBuffer, 0);
+        Assert.assertEquals(packedConcurrentHistogram, packedConcurrentHistogram2);
+
+        targetCompressedBuffer = allocator.allocate(packedConcurrentHistogram.getNeededByteBufferCapacity());
+        packedConcurrentHistogram.encodeIntoCompressedByteBuffer(targetCompressedBuffer);
+        targetCompressedBuffer.rewind();
+
+        PackedConcurrentHistogram packedConcurrentHistogram3 = PackedConcurrentHistogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
+        Assert.assertEquals(packedConcurrentHistogram, packedConcurrentHistogram3);
+
         System.out.println("Testing encoding of a AtomicHistogram:");
         targetBuffer = allocator.allocate(atomicHistogram.getNeededByteBufferCapacity());
         atomicHistogram.encodeIntoByteBuffer(targetBuffer);
@@ -187,11 +236,66 @@ public class HistogramEncodingTest {
 
         DoubleHistogram doubleHistogram3 = DoubleHistogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
         Assert.assertEquals(doubleHistogram, doubleHistogram3);
+
+        System.out.println("Testing encoding of a PackedDoubleHistogram:");
+        targetBuffer = allocator.allocate(packedDoubleHistogram.getNeededByteBufferCapacity());
+        packedDoubleHistogram.encodeIntoByteBuffer(targetBuffer);
+        targetBuffer.rewind();
+
+        PackedDoubleHistogram packedDoubleHistogram2 = PackedDoubleHistogram.decodeFromByteBuffer(targetBuffer, 0);
+        Assert.assertEquals(packedDoubleHistogram, packedDoubleHistogram2);
+
+        targetCompressedBuffer = allocator.allocate(packedDoubleHistogram.getNeededByteBufferCapacity());
+        packedDoubleHistogram.encodeIntoCompressedByteBuffer(targetCompressedBuffer);
+        targetCompressedBuffer.rewind();
+
+        PackedDoubleHistogram packedDoubleHistogram3 = PackedDoubleHistogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
+        Assert.assertEquals(packedDoubleHistogram, packedDoubleHistogram3);
+
+        System.out.println("Testing encoding of a ConcurrentDoubleHistogram:");
+        targetBuffer = allocator.allocate(concurrentDoubleHistogram.getNeededByteBufferCapacity());
+        concurrentDoubleHistogram.encodeIntoByteBuffer(targetBuffer);
+        targetBuffer.rewind();
+
+        ConcurrentDoubleHistogram concurrentDoubleHistogram2 = ConcurrentDoubleHistogram.decodeFromByteBuffer(targetBuffer, 0);
+        Assert.assertEquals(concurrentDoubleHistogram, concurrentDoubleHistogram2);
+
+        targetCompressedBuffer = allocator.allocate(concurrentDoubleHistogram.getNeededByteBufferCapacity());
+        concurrentDoubleHistogram.encodeIntoCompressedByteBuffer(targetCompressedBuffer);
+        targetCompressedBuffer.rewind();
+
+        ConcurrentDoubleHistogram concurrentDoubleHistogram3 = ConcurrentDoubleHistogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
+        Assert.assertEquals(concurrentDoubleHistogram, concurrentDoubleHistogram3);
+
+        System.out.println("Testing encoding of a PackedConcurrentDoubleHistogram:");
+        targetBuffer = allocator.allocate(packedConcurrentDoubleHistogram.getNeededByteBufferCapacity());
+        packedConcurrentDoubleHistogram.encodeIntoByteBuffer(targetBuffer);
+        targetBuffer.rewind();
+
+        PackedConcurrentDoubleHistogram packedConcurrentDoubleHistogram2 = PackedConcurrentDoubleHistogram.decodeFromByteBuffer(targetBuffer, 0);
+        Assert.assertEquals(packedConcurrentDoubleHistogram, packedConcurrentDoubleHistogram2);
+
+        targetCompressedBuffer = allocator.allocate(packedConcurrentDoubleHistogram.getNeededByteBufferCapacity());
+        packedConcurrentDoubleHistogram.encodeIntoCompressedByteBuffer(targetCompressedBuffer);
+        targetCompressedBuffer.rewind();
+
+        PackedConcurrentDoubleHistogram packedConcurrentDoubleHistogram3 = PackedConcurrentDoubleHistogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
+        Assert.assertEquals(packedConcurrentDoubleHistogram, packedConcurrentDoubleHistogram3);
     }
 
-    @Test
-    public void testSimpleIntegerHistogramEncoding() throws Exception {
-        Histogram histogram = new Histogram(274877906943L, 3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            AtomicHistogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSimpleIntegerHistogramEncoding(final Class histoClass) throws Exception {
+        AbstractHistogram histogram = constructHistogram(histoClass, 274877906943L, 3);
         histogram.recordValue(6147);
         histogram.recordValue(1024);
         histogram.recordValue(0);
@@ -200,7 +304,7 @@ public class HistogramEncodingTest {
 
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
-        Histogram decodedHistogram = Histogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
+        AbstractHistogram decodedHistogram = decodeFromCompressedByteBuffer(histoClass, targetBuffer, 0);
         Assert.assertEquals(histogram, decodedHistogram);
 
         histogram.recordValueWithCount(100, 1L << 4); // Make total count > 2^4
@@ -208,15 +312,18 @@ public class HistogramEncodingTest {
         targetBuffer.clear();
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
-        decodedHistogram = Histogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
+        decodedHistogram = decodeFromCompressedByteBuffer(histoClass, targetBuffer, 0);
         Assert.assertEquals(histogram, decodedHistogram);
 
+        if (histoClass.equals(ShortCountsHistogram.class)) {
+            return; // Going farther will overflow short counts histogram
+        }
         histogram.recordValueWithCount(200, 1L << 16); // Make total count > 2^16
 
         targetBuffer.clear();
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
-        decodedHistogram = Histogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
+        decodedHistogram = decodeFromCompressedByteBuffer(histoClass, targetBuffer, 0);
         Assert.assertEquals(histogram, decodedHistogram);
 
         histogram.recordValueWithCount(300, 1L << 20); // Make total count > 2^20
@@ -224,29 +331,39 @@ public class HistogramEncodingTest {
         targetBuffer.clear();
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
-        decodedHistogram = Histogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
+        decodedHistogram = decodeFromCompressedByteBuffer(histoClass, targetBuffer, 0);
         Assert.assertEquals(histogram, decodedHistogram);
 
+        if (histoClass.equals(IntCountsHistogram.class)) {
+            return; // Going farther will overflow int counts histogram
+        }
         histogram.recordValueWithCount(400, 1L << 32); // Make total count > 2^32
 
         targetBuffer.clear();
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
-        decodedHistogram = Histogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
-        Assert.assertEquals(histogram, decodedHistogram);
+        decodedHistogram = decodeFromCompressedByteBuffer(histoClass, targetBuffer, 0);
+         Assert.assertEquals(histogram, decodedHistogram);
 
         histogram.recordValueWithCount(500, 1L << 52); // Make total count > 2^52
 
         targetBuffer.clear();
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
-        decodedHistogram = Histogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
+        decodedHistogram = decodeFromCompressedByteBuffer(histoClass, targetBuffer, 0);
         Assert.assertEquals(histogram, decodedHistogram);
     }
 
-    @Test
-    public void testSimpleDoubleHistogramEncoding() throws Exception {
-        DoubleHistogram histogram = new DoubleHistogram(100000000L, 3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            DoubleHistogram.class,
+            SynchronizedDoubleHistogram.class,
+            ConcurrentDoubleHistogram.class,
+            PackedDoubleHistogram.class,
+            PackedConcurrentDoubleHistogram.class,
+    })
+    public void testSimpleDoubleHistogramEncoding(final Class histoClass) throws Exception {
+        DoubleHistogram histogram = constructDoubleHistogram(histoClass, 100000000L, 3);
         histogram.recordValue(6.0);
         histogram.recordValue(1.0);
         histogram.recordValue(0.0);
@@ -255,14 +372,23 @@ public class HistogramEncodingTest {
         histogram.encodeIntoCompressedByteBuffer(targetBuffer);
         targetBuffer.rewind();
 
-        DoubleHistogram decodedHistogram = DoubleHistogram.decodeFromCompressedByteBuffer(targetBuffer, 0);
+        DoubleHistogram decodedHistogram = decodeDoubleHistogramFromCompressedByteBuffer(histoClass, targetBuffer, 0);
 
         Assert.assertEquals(histogram, decodedHistogram);
     }
 
-    @Test
-    public void testResizingHistogramBetweenCompressedEncodings() throws Exception {
-        Histogram histogram = new Histogram(3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testResizingHistogramBetweenCompressedEncodings(final Class histoClass) throws Exception {
+        AbstractHistogram histogram = constructHistogram(histoClass, 3);
 
         histogram.recordValue(1);
 
@@ -275,7 +401,7 @@ public class HistogramEncodingTest {
         histogram.encodeIntoCompressedByteBuffer(targetCompressedBuffer);
         targetCompressedBuffer.rewind();
 
-        Histogram histogram2 = Histogram.decodeFromCompressedByteBuffer(targetCompressedBuffer, 0);
+        AbstractHistogram histogram2 = decodeFromCompressedByteBuffer(histoClass, targetCompressedBuffer, 0);
         Assert.assertEquals(histogram, histogram2);
     }
 }
diff --git a/src/test/java/org/HdrHistogram/HistogramLogReaderWriterTest.java b/src/test/java/org/HdrHistogram/HistogramLogReaderWriterTest.java
index dfe999b..58dc45d 100644
--- a/src/test/java/org/HdrHistogram/HistogramLogReaderWriterTest.java
+++ b/src/test/java/org/HdrHistogram/HistogramLogReaderWriterTest.java
@@ -1,7 +1,7 @@
 package org.HdrHistogram;
 
-import junit.framework.Assert;
-import org.junit.Test;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -27,7 +27,7 @@ public class HistogramLogReaderWriterTest {
         HistogramLogReader reader = new HistogramLogReader(readerStream);
         EncodableHistogram histogram = reader.nextIntervalHistogram();
         Assert.assertNull(histogram);
-        Assert.assertEquals(1.0, reader.getStartTimeSec());
+        Assert.assertEquals(1.0, reader.getStartTimeSec(), 0.000001);
     }
 
     @Test
@@ -75,7 +75,7 @@ public class HistogramLogReaderWriterTest {
         Assert.assertEquals(48761, totalCount);
         Assert.assertEquals(1745879039, accumulatedHistogram.getValueAtPercentile(99.9));
         Assert.assertEquals(1796210687, accumulatedHistogram.getMaxValue());
-        Assert.assertEquals(1441812279.474, reader.getStartTimeSec());
+        Assert.assertEquals(1441812279.474, reader.getStartTimeSec(), 0.000001);
 
         readerStream = HistogramLogReaderWriterTest.class.getResourceAsStream("jHiccup-2.0.7S.logV2.hlog");
         reader = new HistogramLogReader(readerStream);
@@ -129,7 +129,7 @@ public class HistogramLogReaderWriterTest {
         Assert.assertEquals(65964, totalCount);
         Assert.assertEquals(1829765119, accumulatedHistogram.getValueAtPercentile(99.9));
         Assert.assertEquals(1888485375, accumulatedHistogram.getMaxValue());
-        Assert.assertEquals(1438867590.285, reader.getStartTimeSec());
+        Assert.assertEquals(1438867590.285, reader.getStartTimeSec(), 0.000001);
 
         readerStream = HistogramLogReaderWriterTest.class.getResourceAsStream("jHiccup-2.0.6.logV1.hlog");
         reader = new HistogramLogReader(readerStream);
@@ -183,7 +183,7 @@ public class HistogramLogReaderWriterTest {
         Assert.assertEquals(61256, totalCount);
         Assert.assertEquals(1510998015, accumulatedHistogram.getValueAtPercentile(99.9));
         Assert.assertEquals(1569718271, accumulatedHistogram.getMaxValue());
-        Assert.assertEquals(1438869961.225, reader.getStartTimeSec());
+        Assert.assertEquals(1438869961.225, reader.getStartTimeSec(), 0.000001);
 
         readerStream = HistogramLogReaderWriterTest.class.getResourceAsStream("jHiccup-2.0.1.logV0.hlog");
         reader = new HistogramLogReader(readerStream);
@@ -237,7 +237,7 @@ public class HistogramLogReaderWriterTest {
         Assert.assertEquals(300056, totalCount);
         Assert.assertEquals(1214463, accumulatedHistogram.getValueAtPercentile(99.9));
         Assert.assertEquals(1546239, accumulatedHistogram.getMaxValue());
-        Assert.assertEquals(1438613579.295, reader.getStartTimeSec());
+        Assert.assertEquals(1438613579.295, reader.getStartTimeSec(), 0.000001);
 
         readerStream = HistogramLogReaderWriterTest.class.getResourceAsStream("ycsb.logV1.hlog");
         reader = new HistogramLogReader(readerStream);
@@ -296,7 +296,7 @@ public class HistogramLogReaderWriterTest {
         FileInputStream readerStream = new FileInputStream(temp);
         HistogramLogReader reader = new HistogramLogReader(readerStream);
         Histogram histogram = (Histogram) reader.nextIntervalHistogram();
-        Assert.assertEquals(11.0, reader.getStartTimeSec());
+        Assert.assertEquals(11.0, reader.getStartTimeSec(), 0.000001);
         Assert.assertNotNull(histogram);
         Assert.assertEquals(0, histogram.getTotalCount());
         Assert.assertEquals(11100, histogram.getStartTimeStamp());
@@ -313,7 +313,7 @@ public class HistogramLogReaderWriterTest {
         reader = new HistogramLogReader(readerStream);
         // relative read from the file, should include both histograms
         histogram = (Histogram) reader.nextIntervalHistogram(0.0, 4.0);
-        Assert.assertEquals(11.0, reader.getStartTimeSec());
+        Assert.assertEquals(11.0, reader.getStartTimeSec(), 0.000001);
         Assert.assertNotNull(histogram);
         Assert.assertEquals(0, histogram.getTotalCount());
         Assert.assertEquals(11100, histogram.getStartTimeStamp());
@@ -330,7 +330,7 @@ public class HistogramLogReaderWriterTest {
         reader = new HistogramLogReader(readerStream);
         // relative read from the file, should skip first histogram
         histogram = (Histogram) reader.nextIntervalHistogram(1.0, 4.0);
-        Assert.assertEquals(11.0, reader.getStartTimeSec());
+        Assert.assertEquals(11.0, reader.getStartTimeSec(), 0.000001);
         Assert.assertNotNull(histogram);
         Assert.assertEquals(0, histogram.getTotalCount());
         Assert.assertEquals(12100, histogram.getStartTimeStamp());
diff --git a/src/test/java/org/HdrHistogram/HistogramShiftTest.java b/src/test/java/org/HdrHistogram/HistogramShiftTest.java
index 66dcb5d..a5e4677 100644
--- a/src/test/java/org/HdrHistogram/HistogramShiftTest.java
+++ b/src/test/java/org/HdrHistogram/HistogramShiftTest.java
@@ -10,6 +10,10 @@ package org.HdrHistogram;
 
 import org.junit.Assert;
 import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.HdrHistogram.HistogramTestUtils.constructHistogram;
 
 /**
  * JUnit test for {@link Histogram}
@@ -17,48 +21,29 @@ import org.junit.Test;
 public class HistogramShiftTest {
     static final long highestTrackableValue = 3600L * 1000 * 1000; // e.g. for 1 hr in usec units
 
-    @Test
-    public void testHistogramShift() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, 3);
+    static final Class[] histogramClassesNoAtomic = {
+            Histogram.class, ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class, PackedConcurrentHistogram.class
+    };
+
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testHistogramShift(Class histoClass) throws Exception {
+        // Histogram h = new Histogram(1L, 1L << 32, 3);
+        AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, 3);
         testShiftLowestBucket(histogram);
         testShiftNonLowestBucket(histogram);
     }
 
-    @Test
-    public void testIntHistogramShift() throws Exception {
-        IntCountsHistogram intCountsHistogram = new IntCountsHistogram(highestTrackableValue, 3);
-        testShiftLowestBucket(intCountsHistogram);
-        testShiftNonLowestBucket(intCountsHistogram);
-    }
-
-    @Test
-    public void testShortHistogramShift() throws Exception {
-        ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, 3);
-        testShiftLowestBucket(shortCountsHistogram);
-        testShiftNonLowestBucket(shortCountsHistogram);
-    }
-
-    @Test
-    public void testSynchronizedHistogramShift() throws Exception {
-        SynchronizedHistogram synchronizedHistogram = new SynchronizedHistogram(highestTrackableValue, 3);
-        testShiftLowestBucket(synchronizedHistogram);
-        testShiftNonLowestBucket(synchronizedHistogram);
-    }
-
-    @Test(expected = IllegalStateException.class)
-    public void testAtomicHistogramShift() throws Exception {
-        AtomicHistogram atomicHistogram = new AtomicHistogram(highestTrackableValue, 3);
-        testShiftLowestBucket(atomicHistogram);
-        testShiftNonLowestBucket(atomicHistogram);
-    }
-
-    @Test
-    public void testConcurrentHistogramShift() throws Exception {
-        ConcurrentHistogram concurrentHistogram = new ConcurrentHistogram(highestTrackableValue, 3);
-        testShiftLowestBucket(concurrentHistogram);
-        testShiftNonLowestBucket(concurrentHistogram);
-    }
-
     void testShiftLowestBucket(AbstractHistogram histogram) {
         for (int shiftAmount = 0; shiftAmount < 10; shiftAmount++) {
             histogram.reset();
diff --git a/src/test/java/org/HdrHistogram/HistogramTest.java b/src/test/java/org/HdrHistogram/HistogramTest.java
index cb6e12d..ef69b6b 100644
--- a/src/test/java/org/HdrHistogram/HistogramTest.java
+++ b/src/test/java/org/HdrHistogram/HistogramTest.java
@@ -8,34 +8,51 @@
 
 package org.HdrHistogram;
 
-import org.junit.*;
+import org.junit.Assert;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
 import java.io.*;
-import java.util.Random;
 import java.util.zip.Deflater;
 
-import java.io.ByteArrayOutputStream;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import static org.HdrHistogram.HistogramTestUtils.constructHistogram;
+
 /**
  * JUnit test for {@link Histogram}
  */
 public class HistogramTest {
     static final long highestTrackableValue = 3600L * 1000 * 1000; // e.g. for 1 hr in usec units
     static final int numberOfSignificantValueDigits = 3;
-    // static final long testValueLevel = 12340;
     static final long testValueLevel = 4;
 
-    @Test
-    public void testConstructionArgumentRanges() throws Exception {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testConstructionArgumentRanges(Class histoClass) throws Exception {
         Boolean thrown = false;
-        Histogram histogram = null;
+        AbstractHistogram histogram = null;
 
         try {
             // This should throw:
-            histogram = new Histogram(1, numberOfSignificantValueDigits);
+            // histogram = new Histogram(1, numberOfSignificantValueDigits);
+            histogram = constructHistogram(histoClass, 1, numberOfSignificantValueDigits);
         } catch (IllegalArgumentException e) {
             thrown = true;
         }
@@ -45,7 +62,8 @@ public class HistogramTest {
         thrown = false;
         try {
             // This should throw:
-            histogram = new Histogram(highestTrackableValue, 6);
+            // histogram = new Histogram(highestTrackableValue, 6);
+            histogram = constructHistogram(histoClass, highestTrackableValue, 6);
         } catch (IllegalArgumentException e) {
             thrown = true;
         }
@@ -55,7 +73,8 @@ public class HistogramTest {
         thrown = false;
         try {
             // This should throw:
-            histogram = new Histogram(highestTrackableValue, -1);
+            // histogram = new Histogram(highestTrackableValue, -1);
+            histogram = constructHistogram(histoClass, highestTrackableValue, -1);
         } catch (IllegalArgumentException e) {
             thrown = true;
         }
@@ -63,9 +82,20 @@ public class HistogramTest {
         Assert.assertEquals(histogram, null);
     }
 
-    @Test
-    public void testUnitMagnitude0IndexCalculations() {
-        Histogram h = new Histogram(1L, 1L << 32, 3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testUnitMagnitude0IndexCalculations(Class histoClass) {
+        // Histogram h = new Histogram(1L, 1L << 32, 3);
+        AbstractHistogram h = constructHistogram(histoClass, 1L, 1L << 32, 3);
         assertEquals(2048, h.subBucketCount);
         assertEquals(0, h.unitMagnitude);
         // subBucketCount = 2^11, so 2^11 << 22 is > the max of 2^32 for 23 buckets total
@@ -94,9 +124,20 @@ public class HistogramTest {
         assertEquals(1024 + 3, h.getSubBucketIndex((2048L << 22) + 3 * (1 << 23), 23));
     }
 
-    @Test
-    public void testUnitMagnitude4IndexCalculations() {
-        Histogram h = new Histogram(1L << 12, 1L << 32, 3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testUnitMagnitude4IndexCalculations(Class histoClass) {
+        // Histogram h = new Histogram(1L << 12, 1L << 32, 3);
+        AbstractHistogram h = constructHistogram(histoClass, 1L << 12, 1L << 32, 3);
         assertEquals(2048, h.subBucketCount);
         assertEquals(12, h.unitMagnitude);
         // subBucketCount = 2^11. With unit magnitude shift, it's 2^23. 2^23 << 10 is > the max of 2^32 for 11 buckets
@@ -134,10 +175,21 @@ public class HistogramTest {
         assertEquals(1024 + 3, h.getSubBucketIndex((unit << 21) + 3 * (unit << 11), 11));
     }
 
-    @Test
-    public void testUnitMagnitude51SubBucketMagnitude11IndexCalculations() {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testUnitMagnitude51SubBucketMagnitude11IndexCalculations(Class histoClass) {
         // maximum unit magnitude for this precision
-        Histogram h = new Histogram(1L << 51, Long.MAX_VALUE, 3);
+        // Histogram h = new Histogram(1L << 51, Long.MAX_VALUE, 3);
+        AbstractHistogram h = constructHistogram(histoClass, 1L << 51, Long.MAX_VALUE, 3);
         assertEquals(2048, h.subBucketCount);
         assertEquals(51, h.unitMagnitude);
         // subBucketCount = 2^11. With unit magnitude shift, it's 2^62. 1 more bucket to (almost) reach 2^63.
@@ -173,10 +225,21 @@ public class HistogramTest {
         assertEquals(1024 + 1023, h.getSubBucketIndex(Long.MAX_VALUE, 1));
     }
 
-    @Test
-    public void testUnitMagnitude52SubBucketMagnitude11Throws() {
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testUnitMagnitude52SubBucketMagnitude11Throws(Class histoClass) {
         try {
-            new Histogram(1L << 52, 1L << 62, 3);
+            // new Histogram(1L << 52, 1L << 62, 3);
+            constructHistogram(histoClass, 1L << 52, 1L << 62, 3);
             fail();
         } catch (IllegalArgumentException e) {
             assertEquals("Cannot represent numberOfSignificantValueDigits worth of values beyond lowestDiscernibleValue",
@@ -184,9 +247,20 @@ public class HistogramTest {
         }
     }
 
-    @Test
-    public void testUnitMagnitude54SubBucketMagnitude8Ok() {
-        Histogram h = new Histogram(1L << 54, 1L << 62, 2);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testUnitMagnitude54SubBucketMagnitude8Ok(Class histoClass) {
+        // Histogram h = new Histogram(1L << 54, 1L << 62, 2);
+        AbstractHistogram h = constructHistogram(histoClass, 1L << 54, 1L << 62, 2);
         assertEquals(256, h.subBucketCount);
         assertEquals(54, h.unitMagnitude);
         // subBucketCount = 2^8. With unit magnitude shift, it's 2^62.
@@ -201,9 +275,20 @@ public class HistogramTest {
         assertEquals(128 + 127, h.getSubBucketIndex(Long.MAX_VALUE, 1));
     }
 
-    @Test
-    public void testUnitMagnitude61SubBucketMagnitude0Ok() {
-        Histogram h = new Histogram(1L << 61, 1L << 62, 0);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testUnitMagnitude61SubBucketMagnitude0Ok(Class histoClass) {
+        // Histogram h = new Histogram(1L << 61, 1L << 62, 0);
+        AbstractHistogram h = constructHistogram(histoClass, 1L << 61, 1L << 62, 0);
         assertEquals(2, h.subBucketCount);
         assertEquals(61, h.unitMagnitude);
         // subBucketCount = 2^1. With unit magnitude shift, it's 2^62. 1 more bucket to be > the max of 2^62.
@@ -218,9 +303,19 @@ public class HistogramTest {
         assertEquals(1, h.getSubBucketIndex(Long.MAX_VALUE, 1));
     }
 
-    @Test
-    public void testEmptyHistogram() throws Exception {
-        Histogram histogram = new Histogram(3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testEmptyHistogram(Class histoClass) throws Exception {
+        // Histogram histogram = new Histogram(3);
+        AbstractHistogram histogram = constructHistogram(histoClass, 3);
         long min = histogram.getMinValue();
         Assert.assertEquals(0, min);
         long max = histogram.getMaxValue();
@@ -233,449 +328,716 @@ public class HistogramTest {
         Assert.assertEquals(100.0, pcnt, 0.0000000000001D);
     }
 
-    @Test
-    public void testConstructionArgumentGets() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testConstructionArgumentGets(Class histoClass) throws Exception {
+        // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+        AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
         Assert.assertEquals(1, histogram.getLowestDiscernibleValue());
         Assert.assertEquals(highestTrackableValue, histogram.getHighestTrackableValue());
         Assert.assertEquals(numberOfSignificantValueDigits, histogram.getNumberOfSignificantValueDigits());
-        Histogram histogram2 = new Histogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
+        // Histogram histogram2 = new Histogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
+        AbstractHistogram histogram2 = constructHistogram(histoClass, 1000, highestTrackableValue, numberOfSignificantValueDigits);
         Assert.assertEquals(1000, histogram2.getLowestDiscernibleValue());
         verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testGetEstimatedFootprintInBytes() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        /*
-        *     largestValueWithSingleUnitResolution = 2 * (10 ^ numberOfSignificantValueDigits);
-        *     subBucketSize = roundedUpToNearestPowerOf2(largestValueWithSingleUnitResolution);
-
-        *     expectedHistogramFootprintInBytes = 512 +
-        *          ({primitive type size} / 2) *
-        *          (log2RoundedUp((trackableValueRangeSize) / subBucketSize) + 2) *
-        *          subBucketSize
-        */
-        long largestValueWithSingleUnitResolution = 2 * (long) Math.pow(10, numberOfSignificantValueDigits);
-        int subBucketCountMagnitude = (int) Math.ceil(Math.log(largestValueWithSingleUnitResolution)/Math.log(2));
-        int subBucketSize = (int) Math.pow(2, (subBucketCountMagnitude));
-
-        long expectedSize = 512 +
-                ((8 *
-                 ((long)(
-                        Math.ceil(
-                         Math.log(highestTrackableValue / subBucketSize)
-                                 / Math.log(2)
-                        )
-                       + 2)) *
-                    (1 << (64 - Long.numberOfLeadingZeros(2 * (long) Math.pow(10, numberOfSignificantValueDigits))))
-                 ) / 2);
-        Assert.assertEquals(expectedSize, histogram.getEstimatedFootprintInBytes());
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+    })
+    public void testGetEstimatedFootprintInBytes(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            /*
+             *     largestValueWithSingleUnitResolution = 2 * (10 ^ numberOfSignificantValueDigits);
+             *     subBucketSize = roundedUpToNearestPowerOf2(largestValueWithSingleUnitResolution);
+
+             *     expectedHistogramFootprintInBytes = 512 +
+             *          ({primitive type size} / 2) *
+             *          (log2RoundedUp((trackableValueRangeSize) / subBucketSize) + 2) *
+             *          subBucketSize
+             */
+            long largestValueWithSingleUnitResolution = 2 * (long) Math.pow(10, numberOfSignificantValueDigits);
+            int subBucketCountMagnitude = (int) Math.ceil(Math.log(largestValueWithSingleUnitResolution) / Math.log(2));
+            int subBucketSize = (int) Math.pow(2, (subBucketCountMagnitude));
+            long expectedSize = 512 +
+                    ((8 *
+                            ((long) (
+                                    Math.ceil(
+                                            Math.log(highestTrackableValue / subBucketSize)
+                                                    / Math.log(2)
+                                    )
+                                            + 2)) *
+                            (1 << (64 - Long.numberOfLeadingZeros(2 * (long) Math.pow(10, numberOfSignificantValueDigits))))
+                    ) / 2);
+            Assert.assertEquals(expectedSize, histogram.getEstimatedFootprintInBytes());
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testRecordValue() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        Assert.assertEquals(1L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(1L, histogram.getTotalCount());
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testRecordValue(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            Assert.assertEquals(1L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(1L, histogram.getTotalCount());
+            verifyMaxValue(histogram);
     }
 
-    @Test(expected = ArrayIndexOutOfBoundsException.class)
-    public void testRecordValue_Overflow_ShouldThrowException() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(highestTrackableValue * 3);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testRecordValue_Overflow_ShouldThrowException(final Class histoClass) throws Exception {
+        Assertions.assertThrows(ArrayIndexOutOfBoundsException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+                        AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+                        histogram.recordValue(highestTrackableValue * 3);
+                    }
+                });
     }
 
-
-    @Test
-    public void testConstructionWithLargeNumbers() throws Exception {
-        Histogram histogram = new Histogram(20000000, 100000000, 5);
-        histogram.recordValue(100000000);
-        histogram.recordValue(20000000);
-        histogram.recordValue(30000000);
-        Assert.assertTrue(histogram.valuesAreEquivalent(20000000, histogram.getValueAtPercentile(50.0)));
-        Assert.assertTrue(histogram.valuesAreEquivalent(30000000, histogram.getValueAtPercentile(50.0)));
-        Assert.assertTrue(histogram.valuesAreEquivalent(100000000, histogram.getValueAtPercentile(83.33)));
-        Assert.assertTrue(histogram.valuesAreEquivalent(100000000, histogram.getValueAtPercentile(83.34)));
-        Assert.assertTrue(histogram.valuesAreEquivalent(100000000, histogram.getValueAtPercentile(99.0)));
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testConstructionWithLargeNumbers(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(20000000, 100000000, 5);
+            AbstractHistogram histogram = constructHistogram(histoClass, 20000000, 100000000, 5);
+            histogram.recordValue(100000000);
+            histogram.recordValue(20000000);
+            histogram.recordValue(30000000);
+            Assert.assertTrue(histogram.valuesAreEquivalent(20000000, histogram.getValueAtPercentile(50.0)));
+            Assert.assertTrue(histogram.valuesAreEquivalent(30000000, histogram.getValueAtPercentile(50.0)));
+            Assert.assertTrue(histogram.valuesAreEquivalent(100000000, histogram.getValueAtPercentile(83.33)));
+            Assert.assertTrue(histogram.valuesAreEquivalent(100000000, histogram.getValueAtPercentile(83.34)));
+            Assert.assertTrue(histogram.valuesAreEquivalent(100000000, histogram.getValueAtPercentile(99.0)));
     }
 
-    @Test
-    public void testValueAtPercentileMatchesPercentile() throws Exception {
-        Histogram histogram = new Histogram(1, Long.MAX_VALUE, 3);
-        long[] lengths = {1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000};
-
-
-        for (long length: lengths) {
-            histogram.reset();
-            for (long value = 1; value <= length; value++) {
-                histogram.recordValue(value);
-            }
-            
-            for (long value = 1; value <= length; value++) {
-                Double calculatedPercentile = 100.0 * ((double) value)/length;
-                long lookupValue = histogram.getValueAtPercentile(calculatedPercentile);
-                Assert.assertTrue("length:" + length + " value: " + value + " calculatedPercentile:" + calculatedPercentile +
-                        " getValueAtPercentile(" + calculatedPercentile + ") = " + lookupValue +
-                        " [should be " + value + "]",
-                        histogram.valuesAreEquivalent(value, lookupValue));
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testValueAtPercentileMatchesPercentile(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(1, Long.MAX_VALUE, 3);
+            AbstractHistogram histogram = constructHistogram(histoClass, 1, Long.MAX_VALUE, 2);
+            long[] lengths = {1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000};
+
+            for (long length : lengths) {
+                histogram.reset();
+                for (long value = 1; value <= length; value++) {
+                    histogram.recordValue(value);
+                }
+
+                for (long value = 1; value <= length; value = histogram.nextNonEquivalentValue(value)) {
+                    Double calculatedPercentile = 100.0 * ((double) value) / length;
+                    long lookupValue = histogram.getValueAtPercentile(calculatedPercentile);
+                    Assert.assertTrue("length:" + length + " value: " + value + " calculatedPercentile:" + calculatedPercentile +
+                                    " getValueAtPercentile(" + calculatedPercentile + ") = " + lookupValue +
+                                    " [should be " + value + "]",
+                            histogram.valuesAreEquivalent(value, lookupValue));
+                }
             }
-        }
     }
 
-    @Test
-    public void testValueAtPercentileMatchesPercentileIter() throws Exception {
-        Histogram histogram = new Histogram(1, Long.MAX_VALUE, 3);
-        long[] lengths = {1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000};
-
-
-        for (long length: lengths) {
-            histogram.reset();
-            for (long value = 1; value <= length; value++) {
-                histogram.recordValue(value);
-            }
-
-            int percentileTicksPerHalfDistance = 1000;
-            for (HistogramIterationValue v : histogram.percentiles(percentileTicksPerHalfDistance)) {
-                long calculatedValue = histogram.getValueAtPercentile(v.getPercentile());
-                long iterValue = v.getValueIteratedTo();
-                Assert.assertTrue("length:" + length + " percentile: " + v.getPercentile() +
-                                " calculatedValue:" + calculatedValue + " iterValue:" + iterValue +
-                                "[should be " + calculatedValue + "]",
-                        histogram.valuesAreEquivalent(calculatedValue, iterValue));
-                Assert.assertTrue(histogram.valuesAreEquivalent(calculatedValue, iterValue));
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testValueAtPercentileMatchesPercentileIter(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(1, Long.MAX_VALUE, 3);
+            AbstractHistogram histogram = constructHistogram(histoClass, 1, Long.MAX_VALUE, 2);
+            long[] lengths = {1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000};
+
+            for (long length : lengths) {
+                histogram.reset();
+                for (long value = 1; value <= length; value++) {
+                    histogram.recordValue(value);
+                }
+
+                int percentileTicksPerHalfDistance = 1000;
+                for (HistogramIterationValue v : histogram.percentiles(percentileTicksPerHalfDistance)) {
+                    long calculatedValue = histogram.getValueAtPercentile(v.getPercentile());
+                    long iterValue = v.getValueIteratedTo();
+                    Assert.assertTrue("length:" + length + " percentile: " + v.getPercentile() +
+                                    " calculatedValue:" + calculatedValue + " iterValue:" + iterValue +
+                                    "[should be " + calculatedValue + "]",
+                            histogram.valuesAreEquivalent(calculatedValue, iterValue));
+                    Assert.assertTrue(histogram.valuesAreEquivalent(calculatedValue, iterValue));
+                }
             }
-        }
     }
 
-    @Test
-    public void testRecordValueWithExpectedInterval() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValueWithExpectedInterval(testValueLevel, testValueLevel/4);
-        Histogram rawHistogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        rawHistogram.recordValue(testValueLevel);
-        // The data will include corrected samples:
-        Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 1 )/4));
-        Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 2 )/4));
-        Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 3 )/4));
-        Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 4 )/4));
-        Assert.assertEquals(4L, histogram.getTotalCount());
-        // But the raw data will not:
-        Assert.assertEquals(0L, rawHistogram.getCountAtValue((testValueLevel * 1 )/4));
-        Assert.assertEquals(0L, rawHistogram.getCountAtValue((testValueLevel * 2 )/4));
-        Assert.assertEquals(0L, rawHistogram.getCountAtValue((testValueLevel * 3 )/4));
-        Assert.assertEquals(1L, rawHistogram.getCountAtValue((testValueLevel * 4 )/4));
-        Assert.assertEquals(1L, rawHistogram.getTotalCount());
-
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testRecordValueWithExpectedInterval(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValueWithExpectedInterval(testValueLevel, testValueLevel / 4);
+            // Histogram rawHistogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram rawHistogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            rawHistogram.recordValue(testValueLevel);
+            // The data will include corrected samples:
+            Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 1) / 4));
+            Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 2) / 4));
+            Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 3) / 4));
+            Assert.assertEquals(1L, histogram.getCountAtValue((testValueLevel * 4) / 4));
+            Assert.assertEquals(4L, histogram.getTotalCount());
+            // But the raw data will not:
+            Assert.assertEquals(0L, rawHistogram.getCountAtValue((testValueLevel * 1) / 4));
+            Assert.assertEquals(0L, rawHistogram.getCountAtValue((testValueLevel * 2) / 4));
+            Assert.assertEquals(0L, rawHistogram.getCountAtValue((testValueLevel * 3) / 4));
+            Assert.assertEquals(1L, rawHistogram.getCountAtValue((testValueLevel * 4) / 4));
+            Assert.assertEquals(1L, rawHistogram.getTotalCount());
+
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testReset() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(10);
-        histogram.recordValue(100);
-        Assert.assertEquals(histogram.getMinValue(), Math.min(10, testValueLevel));
-        Assert.assertEquals(histogram.getMaxValue(), Math.max(100, testValueLevel));
-        histogram.reset();
-        Assert.assertEquals(0L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(0L, histogram.getTotalCount());
-        verifyMaxValue(histogram);
-        histogram.recordValue(20);
-        histogram.recordValue(80);
-        Assert.assertEquals(histogram.getMinValue(), 20);
-        Assert.assertEquals(histogram.getMaxValue(), 80);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testReset(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(10);
+            histogram.recordValue(100);
+            Assert.assertEquals(histogram.getMinValue(), Math.min(10, testValueLevel));
+            Assert.assertEquals(histogram.getMaxValue(), Math.max(100, testValueLevel));
+            histogram.reset();
+            Assert.assertEquals(0L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(0L, histogram.getTotalCount());
+            verifyMaxValue(histogram);
+            histogram.recordValue(20);
+            histogram.recordValue(80);
+            Assert.assertEquals(histogram.getMinValue(), 20);
+            Assert.assertEquals(histogram.getMaxValue(), 80);
     }
 
-    @Test
-    public void testAdd() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Histogram other = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 1000);
-        other.recordValue(testValueLevel);
-        other.recordValue(testValueLevel * 1000);
-        histogram.add(other);
-        Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(4L, histogram.getTotalCount());
-
-        Histogram biggerOther = new Histogram(highestTrackableValue * 2, numberOfSignificantValueDigits);
-        biggerOther.recordValue(testValueLevel);
-        biggerOther.recordValue(testValueLevel * 1000);
-        biggerOther.recordValue(highestTrackableValue * 2);
-
-        // Adding the smaller histogram to the bigger one should work:
-        biggerOther.add(histogram);
-        Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel));
-        Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(1L, biggerOther.getCountAtValue(highestTrackableValue * 2)); // overflow smaller hist...
-        Assert.assertEquals(7L, biggerOther.getTotalCount());
-
-        // But trying to add a larger histogram into a smaller one should throw an AIOOB:
-        boolean thrown = false;
-        try {
-            // This should throw:
-            histogram.add(biggerOther);
-        } catch (ArrayIndexOutOfBoundsException e) {
-            thrown = true;
-        }
-        Assert.assertTrue(thrown);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testAdd(Class histoClass) throws Exception {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            // Histogram other = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram other = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(testValueLevel * 1000);
+            other.recordValue(testValueLevel);
+            other.recordValue(testValueLevel * 1000);
+            histogram.add(other);
+            Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(4L, histogram.getTotalCount());
+
+            // Histogram biggerOther = new Histogram(highestTrackableValue * 2, numberOfSignificantValueDigits);
+            AbstractHistogram biggerOther = constructHistogram(histoClass, highestTrackableValue * 2, numberOfSignificantValueDigits);
+            biggerOther.recordValue(testValueLevel);
+            biggerOther.recordValue(testValueLevel * 1000);
+            biggerOther.recordValue(highestTrackableValue * 2);
+
+            // Adding the smaller histogram to the bigger one should work:
+            biggerOther.add(histogram);
+            Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel));
+            Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(1L, biggerOther.getCountAtValue(highestTrackableValue * 2)); // overflow smaller hist...
+            Assert.assertEquals(7L, biggerOther.getTotalCount());
+
+            // But trying to add a larger histogram into a smaller one should throw an AIOOB:
+            boolean thrown = false;
+            try {
+                // This should throw:
+                histogram.add(biggerOther);
+            } catch (ArrayIndexOutOfBoundsException e) {
+                thrown = true;
+            }
+            Assert.assertTrue(thrown);
 
-        verifyMaxValue(histogram);
-        verifyMaxValue(other);
-        verifyMaxValue(biggerOther);
+            verifyMaxValue(histogram);
+            verifyMaxValue(other);
+            verifyMaxValue(biggerOther);
     }
 
-    @Test
-    public void testSubtractAfterAdd() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Histogram other = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 1000);
-        other.recordValue(testValueLevel);
-        other.recordValue(testValueLevel * 1000);
-        histogram.add(other);
-        Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(4L, histogram.getTotalCount());
-        histogram.add(other);
-        Assert.assertEquals(3L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(3L, histogram.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(6L, histogram.getTotalCount());
-        histogram.subtract(other);
-        Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(4L, histogram.getTotalCount());
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSubtractAfterAdd(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            // Histogram other = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram other = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(testValueLevel * 1000);
+            other.recordValue(testValueLevel);
+            other.recordValue(testValueLevel * 1000);
+            histogram.add(other);
+            Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(4L, histogram.getTotalCount());
+            histogram.add(other);
+            Assert.assertEquals(3L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(3L, histogram.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(6L, histogram.getTotalCount());
+            histogram.subtract(other);
+            Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(2L, histogram.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(4L, histogram.getTotalCount());
 
-        verifyMaxValue(histogram);
-        verifyMaxValue(other);
+            verifyMaxValue(histogram);
+            verifyMaxValue(other);
     }
 
-    @Test
-    public void testSubtractToZeroCounts() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 1000);
-        Assert.assertEquals(1L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(1L, histogram.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(2L, histogram.getTotalCount());
-
-        // Subtracting down to zero counts should work:
-        histogram.subtract(histogram);
-        Assert.assertEquals(0L, histogram.getCountAtValue(testValueLevel));
-        Assert.assertEquals(0L, histogram.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(0L, histogram.getTotalCount());
-
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSubtractToZeroCounts(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(testValueLevel * 1000);
+            Assert.assertEquals(1L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(1L, histogram.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(2L, histogram.getTotalCount());
+
+            // Subtracting down to zero counts should work:
+            histogram.subtract(histogram);
+            Assert.assertEquals(0L, histogram.getCountAtValue(testValueLevel));
+            Assert.assertEquals(0L, histogram.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(0L, histogram.getTotalCount());
+
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testSubtractToNegativeCountsThrows() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Histogram other = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 1000);
-        other.recordValueWithCount(testValueLevel, 2);
-        other.recordValueWithCount(testValueLevel * 1000, 2);
-
-        try {
-            histogram.subtract(other);
-            fail();
-        } catch (IllegalArgumentException e) {
-            // should throw
-        }
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSubtractToNegativeCountsThrows(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            // Histogram other = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram other = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(testValueLevel * 1000);
+            other.recordValueWithCount(testValueLevel, 2);
+            other.recordValueWithCount(testValueLevel * 1000, 2);
+
+            try {
+                histogram.subtract(other);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // should throw
+            }
 
-        verifyMaxValue(histogram);
-        verifyMaxValue(other);
+            verifyMaxValue(histogram);
+            verifyMaxValue(other);
     }
 
-    @Test
-    public void testSubtractSubtrahendValuesOutsideMinuendRangeThrows() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 1000);
-
-        Histogram biggerOther = new Histogram(highestTrackableValue * 2, numberOfSignificantValueDigits);
-        biggerOther.recordValue(testValueLevel);
-        biggerOther.recordValue(testValueLevel * 1000);
-        biggerOther.recordValue(highestTrackableValue * 2); // outside smaller histogram's range
-
-        try {
-            histogram.subtract(biggerOther);
-            fail();
-        } catch (IllegalArgumentException e) {
-            // should throw
-        }
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSubtractSubtrahendValuesOutsideMinuendRangeThrows(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(testValueLevel * 1000);
+
+            // Histogram biggerOther = new Histogram(highestTrackableValue * 2, numberOfSignificantValueDigits);
+            AbstractHistogram biggerOther = constructHistogram(histoClass, highestTrackableValue * 2, numberOfSignificantValueDigits);
+            biggerOther.recordValue(testValueLevel);
+            biggerOther.recordValue(testValueLevel * 1000);
+            biggerOther.recordValue(highestTrackableValue * 2); // outside smaller histogram's range
+
+            try {
+                histogram.subtract(biggerOther);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // should throw
+            }
 
-        verifyMaxValue(histogram);
-        verifyMaxValue(biggerOther);
+            verifyMaxValue(histogram);
+            verifyMaxValue(biggerOther);
     }
 
-    @Test
-    public void testSubtractSubtrahendValuesInsideMinuendRangeWorks() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 1000);
-
-        Histogram biggerOther = new Histogram(highestTrackableValue * 2, numberOfSignificantValueDigits);
-        biggerOther.recordValue(testValueLevel);
-        biggerOther.recordValue(testValueLevel * 1000);
-        biggerOther.recordValue(highestTrackableValue * 2);
-        biggerOther.add(biggerOther);
-        biggerOther.add(biggerOther);
-        Assert.assertEquals(4L, biggerOther.getCountAtValue(testValueLevel));
-        Assert.assertEquals(4L, biggerOther.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(4L, biggerOther.getCountAtValue(highestTrackableValue * 2)); // overflow smaller hist...
-        Assert.assertEquals(12L, biggerOther.getTotalCount());
-
-        // Subtracting the smaller histogram from the bigger one should work:
-        biggerOther.subtract(histogram);
-        Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel));
-        Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel * 1000));
-        Assert.assertEquals(4L, biggerOther.getCountAtValue(highestTrackableValue * 2)); // overflow smaller hist...
-        Assert.assertEquals(10L, biggerOther.getTotalCount());
-
-        verifyMaxValue(histogram);
-        verifyMaxValue(biggerOther);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSubtractSubtrahendValuesInsideMinuendRangeWorks(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            histogram.recordValue(testValueLevel);
+            histogram.recordValue(testValueLevel * 1000);
+
+            // Histogram biggerOther = new Histogram(highestTrackableValue * 2, numberOfSignificantValueDigits);
+            AbstractHistogram biggerOther = constructHistogram(histoClass, highestTrackableValue * 2, numberOfSignificantValueDigits);
+            biggerOther.recordValue(testValueLevel);
+            biggerOther.recordValue(testValueLevel * 1000);
+            biggerOther.recordValue(highestTrackableValue * 2);
+            biggerOther.add(biggerOther);
+            biggerOther.add(biggerOther);
+            Assert.assertEquals(4L, biggerOther.getCountAtValue(testValueLevel));
+            Assert.assertEquals(4L, biggerOther.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(4L, biggerOther.getCountAtValue(highestTrackableValue * 2)); // overflow smaller hist...
+            Assert.assertEquals(12L, biggerOther.getTotalCount());
+
+            // Subtracting the smaller histogram from the bigger one should work:
+            biggerOther.subtract(histogram);
+            Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel));
+            Assert.assertEquals(3L, biggerOther.getCountAtValue(testValueLevel * 1000));
+            Assert.assertEquals(4L, biggerOther.getCountAtValue(highestTrackableValue * 2)); // overflow smaller hist...
+            Assert.assertEquals(10L, biggerOther.getTotalCount());
+
+            verifyMaxValue(histogram);
+            verifyMaxValue(biggerOther);
     }
 
-    @Test
-    public void testSizeOfEquivalentValueRange() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("Size of equivalent range for value 1 is 1",
-                1, histogram.sizeOfEquivalentValueRange(1));
-        Assert.assertEquals("Size of equivalent range for value 1025 is 1",
-                1, histogram.sizeOfEquivalentValueRange(1025));
-        Assert.assertEquals("Size of equivalent range for value 2047 is 1",
-                1, histogram.sizeOfEquivalentValueRange(2047));
-        Assert.assertEquals("Size of equivalent range for value 2048 is 2",
-                2, histogram.sizeOfEquivalentValueRange(2048));
-        Assert.assertEquals("Size of equivalent range for value 2500 is 2",
-                2, histogram.sizeOfEquivalentValueRange(2500));
-        Assert.assertEquals("Size of equivalent range for value 8191 is 4",
-                4, histogram.sizeOfEquivalentValueRange(8191));
-        Assert.assertEquals("Size of equivalent range for value 8192 is 8",
-                8, histogram.sizeOfEquivalentValueRange(8192));
-        Assert.assertEquals("Size of equivalent range for value 10000 is 8",
-                8, histogram.sizeOfEquivalentValueRange(10000));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testSizeOfEquivalentValueRange(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("Size of equivalent range for value 1 is 1",
+                    1, histogram.sizeOfEquivalentValueRange(1));
+            Assert.assertEquals("Size of equivalent range for value 1025 is 1",
+                    1, histogram.sizeOfEquivalentValueRange(1025));
+            Assert.assertEquals("Size of equivalent range for value 2047 is 1",
+                    1, histogram.sizeOfEquivalentValueRange(2047));
+            Assert.assertEquals("Size of equivalent range for value 2048 is 2",
+                    2, histogram.sizeOfEquivalentValueRange(2048));
+            Assert.assertEquals("Size of equivalent range for value 2500 is 2",
+                    2, histogram.sizeOfEquivalentValueRange(2500));
+            Assert.assertEquals("Size of equivalent range for value 8191 is 4",
+                    4, histogram.sizeOfEquivalentValueRange(8191));
+            Assert.assertEquals("Size of equivalent range for value 8192 is 8",
+                    8, histogram.sizeOfEquivalentValueRange(8192));
+            Assert.assertEquals("Size of equivalent range for value 10000 is 8",
+                    8, histogram.sizeOfEquivalentValueRange(10000));
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testScaledSizeOfEquivalentValueRange() {
-        Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("Size of equivalent range for value 1 * 1024 is 1 * 1024",
-                1 * 1024, histogram.sizeOfEquivalentValueRange(1 * 1024));
-        Assert.assertEquals("Size of equivalent range for value 2500 * 1024 is 2 * 1024",
-                2 * 1024, histogram.sizeOfEquivalentValueRange(2500 * 1024));
-        Assert.assertEquals("Size of equivalent range for value 8191 * 1024 is 4 * 1024",
-                4 * 1024, histogram.sizeOfEquivalentValueRange(8191 * 1024));
-        Assert.assertEquals("Size of equivalent range for value 8192 * 1024 is 8 * 1024",
-                8 * 1024, histogram.sizeOfEquivalentValueRange(8192 * 1024));
-        Assert.assertEquals("Size of equivalent range for value 10000 * 1024 is 8 * 1024",
-                8 * 1024, histogram.sizeOfEquivalentValueRange(10000 * 1024));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testScaledSizeOfEquivalentValueRange(Class histoClass) {
+            // Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, 1024, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("Size of equivalent range for value 1 * 1024 is 1 * 1024",
+                    1 * 1024, histogram.sizeOfEquivalentValueRange(1 * 1024));
+            Assert.assertEquals("Size of equivalent range for value 2500 * 1024 is 2 * 1024",
+                    2 * 1024, histogram.sizeOfEquivalentValueRange(2500 * 1024));
+            Assert.assertEquals("Size of equivalent range for value 8191 * 1024 is 4 * 1024",
+                    4 * 1024, histogram.sizeOfEquivalentValueRange(8191 * 1024));
+            Assert.assertEquals("Size of equivalent range for value 8192 * 1024 is 8 * 1024",
+                    8 * 1024, histogram.sizeOfEquivalentValueRange(8192 * 1024));
+            Assert.assertEquals("Size of equivalent range for value 10000 * 1024 is 8 * 1024",
+                    8 * 1024, histogram.sizeOfEquivalentValueRange(10000 * 1024));
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testLowestEquivalentValue() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("The lowest equivalent value to 10007 is 10000",
-                10000, histogram.lowestEquivalentValue(10007));
-        Assert.assertEquals("The lowest equivalent value to 10009 is 10008",
-                10008, histogram.lowestEquivalentValue(10009));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testLowestEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("The lowest equivalent value to 10007 is 10000",
+                    10000, histogram.lowestEquivalentValue(10007));
+            Assert.assertEquals("The lowest equivalent value to 10009 is 10008",
+                    10008, histogram.lowestEquivalentValue(10009));
+            verifyMaxValue(histogram);
     }
 
-
-    @Test
-    public void testScaledLowestEquivalentValue() {
-        Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("The lowest equivalent value to 10007 * 1024 is 10000 * 1024",
-                10000 * 1024, histogram.lowestEquivalentValue(10007 * 1024));
-        Assert.assertEquals("The lowest equivalent value to 10009 * 1024 is 10008 * 1024",
-                10008 * 1024, histogram.lowestEquivalentValue(10009 * 1024));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testScaledLowestEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, 1024, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("The lowest equivalent value to 10007 * 1024 is 10000 * 1024",
+                    10000 * 1024, histogram.lowestEquivalentValue(10007 * 1024));
+            Assert.assertEquals("The lowest equivalent value to 10009 * 1024 is 10008 * 1024",
+                    10008 * 1024, histogram.lowestEquivalentValue(10009 * 1024));
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testHighestEquivalentValue() {
-        Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("The highest equivalent value to 8180 * 1024 is 8183 * 1024 + 1023",
-                8183 * 1024 + 1023, histogram.highestEquivalentValue(8180 * 1024));
-        Assert.assertEquals("The highest equivalent value to 8187 * 1024 is 8191 * 1024 + 1023",
-                8191 * 1024 + 1023, histogram.highestEquivalentValue(8191 * 1024));
-        Assert.assertEquals("The highest equivalent value to 8193 * 1024 is 8199 * 1024 + 1023",
-                8199 * 1024 + 1023, histogram.highestEquivalentValue(8193 * 1024));
-        Assert.assertEquals("The highest equivalent value to 9995 * 1024 is 9999 * 1024 + 1023",
-                9999 * 1024 + 1023, histogram.highestEquivalentValue(9995 * 1024));
-        Assert.assertEquals("The highest equivalent value to 10007 * 1024 is 10007 * 1024 + 1023",
-                10007 * 1024 + 1023, histogram.highestEquivalentValue(10007 * 1024));
-        Assert.assertEquals("The highest equivalent value to 10008 * 1024 is 10015 * 1024 + 1023",
-                10015 * 1024 + 1023, histogram.highestEquivalentValue(10008 * 1024));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testHighestEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, 1024, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("The highest equivalent value to 8180 * 1024 is 8183 * 1024 + 1023",
+                    8183 * 1024 + 1023, histogram.highestEquivalentValue(8180 * 1024));
+            Assert.assertEquals("The highest equivalent value to 8187 * 1024 is 8191 * 1024 + 1023",
+                    8191 * 1024 + 1023, histogram.highestEquivalentValue(8191 * 1024));
+            Assert.assertEquals("The highest equivalent value to 8193 * 1024 is 8199 * 1024 + 1023",
+                    8199 * 1024 + 1023, histogram.highestEquivalentValue(8193 * 1024));
+            Assert.assertEquals("The highest equivalent value to 9995 * 1024 is 9999 * 1024 + 1023",
+                    9999 * 1024 + 1023, histogram.highestEquivalentValue(9995 * 1024));
+            Assert.assertEquals("The highest equivalent value to 10007 * 1024 is 10007 * 1024 + 1023",
+                    10007 * 1024 + 1023, histogram.highestEquivalentValue(10007 * 1024));
+            Assert.assertEquals("The highest equivalent value to 10008 * 1024 is 10015 * 1024 + 1023",
+                    10015 * 1024 + 1023, histogram.highestEquivalentValue(10008 * 1024));
+            verifyMaxValue(histogram);
     }
 
-
-    @Test
-    public void testScaledHighestEquivalentValue() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("The highest equivalent value to 8180 is 8183",
-                8183, histogram.highestEquivalentValue(8180));
-        Assert.assertEquals("The highest equivalent value to 8187 is 8191",
-                8191, histogram.highestEquivalentValue(8191));
-        Assert.assertEquals("The highest equivalent value to 8193 is 8199",
-                8199, histogram.highestEquivalentValue(8193));
-        Assert.assertEquals("The highest equivalent value to 9995 is 9999",
-                9999, histogram.highestEquivalentValue(9995));
-        Assert.assertEquals("The highest equivalent value to 10007 is 10007",
-                10007, histogram.highestEquivalentValue(10007));
-        Assert.assertEquals("The highest equivalent value to 10008 is 10015",
-                10015, histogram.highestEquivalentValue(10008));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testScaledHighestEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("The highest equivalent value to 8180 is 8183",
+                    8183, histogram.highestEquivalentValue(8180));
+            Assert.assertEquals("The highest equivalent value to 8187 is 8191",
+                    8191, histogram.highestEquivalentValue(8191));
+            Assert.assertEquals("The highest equivalent value to 8193 is 8199",
+                    8199, histogram.highestEquivalentValue(8193));
+            Assert.assertEquals("The highest equivalent value to 9995 is 9999",
+                    9999, histogram.highestEquivalentValue(9995));
+            Assert.assertEquals("The highest equivalent value to 10007 is 10007",
+                    10007, histogram.highestEquivalentValue(10007));
+            Assert.assertEquals("The highest equivalent value to 10008 is 10015",
+                    10015, histogram.highestEquivalentValue(10008));
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testMedianEquivalentValue() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("The median equivalent value to 4 is 4",
-                4, histogram.medianEquivalentValue(4));
-        Assert.assertEquals("The median equivalent value to 5 is 5",
-                5, histogram.medianEquivalentValue(5));
-        Assert.assertEquals("The median equivalent value to 4000 is 4001",
-                4001, histogram.medianEquivalentValue(4000));
-        Assert.assertEquals("The median equivalent value to 8000 is 8002",
-                8002, histogram.medianEquivalentValue(8000));
-        Assert.assertEquals("The median equivalent value to 10007 is 10004",
-                10004, histogram.medianEquivalentValue(10007));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testMedianEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("The median equivalent value to 4 is 4",
+                    4, histogram.medianEquivalentValue(4));
+            Assert.assertEquals("The median equivalent value to 5 is 5",
+                    5, histogram.medianEquivalentValue(5));
+            Assert.assertEquals("The median equivalent value to 4000 is 4001",
+                    4001, histogram.medianEquivalentValue(4000));
+            Assert.assertEquals("The median equivalent value to 8000 is 8002",
+                    8002, histogram.medianEquivalentValue(8000));
+            Assert.assertEquals("The median equivalent value to 10007 is 10004",
+                    10004, histogram.medianEquivalentValue(10007));
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testScaledMedianEquivalentValue() {
-        Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertEquals("The median equivalent value to 4 * 1024 is 4 * 1024 + 512",
-                4 * 1024 + 512, histogram.medianEquivalentValue(4 * 1024));
-        Assert.assertEquals("The median equivalent value to 5 * 1024 is 5 * 1024 + 512",
-                5 * 1024 + 512, histogram.medianEquivalentValue(5 * 1024));
-        Assert.assertEquals("The median equivalent value to 4000 * 1024 is 4001 * 1024",
-                4001 * 1024, histogram.medianEquivalentValue(4000 * 1024));
-        Assert.assertEquals("The median equivalent value to 8000 * 1024 is 8002 * 1024",
-                8002 * 1024, histogram.medianEquivalentValue(8000 * 1024));
-        Assert.assertEquals("The median equivalent value to 10007 * 1024 is 10004 * 1024",
-                10004 * 1024, histogram.medianEquivalentValue(10007 * 1024));
-        verifyMaxValue(histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testScaledMedianEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(1024, highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, 1024, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertEquals("The median equivalent value to 4 * 1024 is 4 * 1024 + 512",
+                    4 * 1024 + 512, histogram.medianEquivalentValue(4 * 1024));
+            Assert.assertEquals("The median equivalent value to 5 * 1024 is 5 * 1024 + 512",
+                    5 * 1024 + 512, histogram.medianEquivalentValue(5 * 1024));
+            Assert.assertEquals("The median equivalent value to 4000 * 1024 is 4001 * 1024",
+                    4001 * 1024, histogram.medianEquivalentValue(4000 * 1024));
+            Assert.assertEquals("The median equivalent value to 8000 * 1024 is 8002 * 1024",
+                    8002 * 1024, histogram.medianEquivalentValue(8000 * 1024));
+            Assert.assertEquals("The median equivalent value to 10007 * 1024 is 10004 * 1024",
+                    10004 * 1024, histogram.medianEquivalentValue(10007 * 1024));
+            verifyMaxValue(histogram);
     }
 
-    @Test
-    public void testNextNonEquivalentValue() {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Assert.assertNotSame(null, histogram);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testNextNonEquivalentValue(Class histoClass) {
+            // Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+            AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+            Assert.assertNotSame(null, histogram);
     }
 
     void testAbstractSerialization(AbstractHistogram histogram) throws Exception {
@@ -741,159 +1103,158 @@ public class HistogramTest {
     }
 
     @Test
-    public void testSerialization() throws Exception {
+    public void testPackedEquivalence() {
         Histogram histogram = new Histogram(highestTrackableValue, 3);
-        testAbstractSerialization(histogram);
-
-        Histogram atomicHistogram = new AtomicHistogram(highestTrackableValue, 3);
-        testAbstractSerialization(atomicHistogram);
-
-        Histogram concurrentHistogram = new ConcurrentHistogram(highestTrackableValue, 3);
-        testAbstractSerialization(concurrentHistogram);
-
-        Histogram synchronizedHistogram = new SynchronizedHistogram(highestTrackableValue, 3);
-        testAbstractSerialization(synchronizedHistogram);
-
-        IntCountsHistogram intCountsHistogram = new IntCountsHistogram(highestTrackableValue, 3);
-        testAbstractSerialization(intCountsHistogram);
+        histogram.recordValueWithExpectedInterval(histogram.getHighestTrackableValue() - 1, 255);
 
-        ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, 3);
-        testAbstractSerialization(shortCountsHistogram);
+        // for each value in a non-packed histogram, record the same in a packed one, and for each
+        // step, verify all values up to that point match.
+
+        // start from the top and go down:
+        Histogram packedHistogram = new PackedHistogram(highestTrackableValue, 3);
+        for (int index = histogram.counts.length - 1; index >= 0; index--) {
+            if (histogram.counts[index] != 0) {
+                packedHistogram.addToCountAtIndex(index, histogram.counts[index]);
+                // Now verify every value up to this point:
+                for (int i = histogram.counts.length - 1; i >= index; i--) {
+                    long histValue = histogram.counts[i];
+                    long packedHistValue;
+                    try {
+                        packedHistValue = packedHistogram.getCountAtIndex(i);
+                    } catch (ArrayIndexOutOfBoundsException ex) {
+                        System.out.println("AIOOB at i = " + i + " : " + ex);
+                        throw ex;
+                    }
+                    if (histValue != packedHistValue) {
+                        // Blow up with assert: (easier to breakpoint this way).
+                        Assert.assertEquals("at insertion index " +  index + ", contents of index " + i +
+                                        " don't match", histValue, packedHistValue);
+                    }
+                }
+            }
+        }
+    }
 
-        histogram = new Histogram(highestTrackableValue, 2);
+    @ParameterizedTest
+    @CsvSource({
+            "Histogram, 3",
+            "Histogram, 2",
+            "ConcurrentHistogram, 3",
+            "ConcurrentHistogram, 2",
+            "AtomicHistogram, 3",
+            "AtomicHistogram, 2",
+            "SynchronizedHistogram, 3",
+            "SynchronizedHistogram, 2",
+            "PackedHistogram, 3",
+            "PackedHistogram, 2",
+            "PackedConcurrentHistogram, 3",
+            "PackedConcurrentHistogram, 2",
+            "IntCountsHistogram, 3",
+            "IntCountsHistogram, 2",
+            "ShortCountsHistogram, 3",
+            "ShortCountsHistogram, 4", // ShortCountsHistogram would overflow with 2
+    })
+    public void testSerialization(ArgumentsAccessor arguments) throws Exception {
+        Class histoClass = Class.forName("org.HdrHistogram." + arguments.getString(0));
+        int digits = arguments.getInteger(1);
+
+        AbstractHistogram histogram = constructHistogram(histoClass, highestTrackableValue, digits);
         testAbstractSerialization(histogram);
-
-        atomicHistogram = new AtomicHistogram(highestTrackableValue, 2);
-        testAbstractSerialization(atomicHistogram);
-
-        concurrentHistogram = new ConcurrentHistogram(highestTrackableValue, 2);
-        testAbstractSerialization(concurrentHistogram);
-
-        synchronizedHistogram = new SynchronizedHistogram(highestTrackableValue, 2);
-        testAbstractSerialization(synchronizedHistogram);
-
-        intCountsHistogram = new IntCountsHistogram(highestTrackableValue, 2);
-        testAbstractSerialization(intCountsHistogram);
-
-        shortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, 4); // With 2 decimal points, shorts overflow here
-        testAbstractSerialization(shortCountsHistogram);
     }
 
-    @Test(expected = IllegalStateException.class)
-    public void testOverflow() throws Exception {
-        ShortCountsHistogram histogram = new ShortCountsHistogram(highestTrackableValue, 2);
-        histogram.recordValue(testValueLevel);
-        histogram.recordValue(testValueLevel * 10);
-        // This should overflow a ShortHistogram:
-        histogram.recordValueWithExpectedInterval(histogram.getHighestTrackableValue() - 1, 500);
+    @Test
+    public void testShortCountsHistogramOverflow() throws Exception {
+        Assertions.assertThrows(IllegalStateException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        ShortCountsHistogram histogram =
+                                new ShortCountsHistogram(highestTrackableValue, 2);
+                        histogram.recordValue(testValueLevel);
+                        histogram.recordValue(testValueLevel * 10);
+                        // This should overflow a ShortHistogram:
+                        histogram.recordValueWithExpectedInterval(
+                                histogram.getHighestTrackableValue() - 1, 500);
+                    }
+                });
     }
-    
+
     @Test
-    public void testCopy() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+    public void testIntCountsHistogramOverflow() throws Exception {
+        Assertions.assertThrows(IllegalStateException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        IntCountsHistogram histogram =
+                                new IntCountsHistogram(highestTrackableValue, 2);
+                        histogram.recordValue(testValueLevel);
+                        histogram.recordValue(testValueLevel * 10);
+                        // This should overflow a ShortHistogram:
+                        histogram.recordValueWithCount(testValueLevel, 10);
+                        histogram.recordValueWithCount(testValueLevel, 10);
+                        histogram.recordValueWithCount(testValueLevel, Integer.MAX_VALUE - 10);
+                    }
+                });
+    }
+
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testCopy(Class histoClass) throws Exception {
+        AbstractHistogram histogram =
+                constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         histogram.recordValue(testValueLevel * 10);
         histogram.recordValueWithExpectedInterval(histogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of Histogram:");
         assertEqual(histogram, histogram.copy());
-
-        IntCountsHistogram intCountsHistogram = new IntCountsHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        intCountsHistogram.recordValue(testValueLevel);
-        intCountsHistogram.recordValue(testValueLevel * 10);
-        intCountsHistogram.recordValueWithExpectedInterval(intCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of IntHistogram:");
-        assertEqual(intCountsHistogram, intCountsHistogram.copy());
-  
-        ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        shortCountsHistogram.recordValue(testValueLevel);
-        shortCountsHistogram.recordValue(testValueLevel * 10);
-        shortCountsHistogram.recordValueWithExpectedInterval(shortCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of ShortHistogram:");
-        assertEqual(shortCountsHistogram, shortCountsHistogram.copy());
-  
-        AtomicHistogram atomicHistogram = new AtomicHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        atomicHistogram.recordValue(testValueLevel);
-        atomicHistogram.recordValue(testValueLevel * 10);
-        atomicHistogram.recordValueWithExpectedInterval(atomicHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of AtomicHistogram:");
-        assertEqual(atomicHistogram, atomicHistogram.copy());
-
-        ConcurrentHistogram concurrentHistogram = new ConcurrentHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        concurrentHistogram.recordValue(testValueLevel);
-        concurrentHistogram.recordValue(testValueLevel * 10);
-        concurrentHistogram.recordValueWithExpectedInterval(concurrentHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of ConcurrentHistogram:");
-        assertEqual(concurrentHistogram, concurrentHistogram.copy());
-  
-        SynchronizedHistogram syncHistogram = new SynchronizedHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        syncHistogram.recordValue(testValueLevel);
-        syncHistogram.recordValue(testValueLevel * 10);
-        syncHistogram.recordValueWithExpectedInterval(syncHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of SynchronizedHistogram:");
-        assertEqual(syncHistogram, syncHistogram.copy());
     }
 
-    @Test
-    public void testScaledCopy() throws Exception {
-        Histogram histogram = new Histogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testScaledCopy(Class histoClass) throws Exception {
+        AbstractHistogram histogram =
+                constructHistogram(histoClass,1000, highestTrackableValue, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         histogram.recordValue(testValueLevel * 10);
         histogram.recordValueWithExpectedInterval(histogram.getHighestTrackableValue() - 1, 31000);
 
         System.out.println("Testing copy of scaled Histogram:");
         assertEqual(histogram, histogram.copy());
-
-        IntCountsHistogram intCountsHistogram = new IntCountsHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        intCountsHistogram.recordValue(testValueLevel);
-        intCountsHistogram.recordValue(testValueLevel * 10);
-        intCountsHistogram.recordValueWithExpectedInterval(intCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of scaled IntHistogram:");
-        assertEqual(intCountsHistogram, intCountsHistogram.copy());
-
-        ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        shortCountsHistogram.recordValue(testValueLevel);
-        shortCountsHistogram.recordValue(testValueLevel * 10);
-        shortCountsHistogram.recordValueWithExpectedInterval(shortCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of scaled ShortHistogram:");
-        assertEqual(shortCountsHistogram, shortCountsHistogram.copy());
-
-        AtomicHistogram atomicHistogram = new AtomicHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        atomicHistogram.recordValue(testValueLevel);
-        atomicHistogram.recordValue(testValueLevel * 10);
-        atomicHistogram.recordValueWithExpectedInterval(atomicHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of scaled AtomicHistogram:");
-        assertEqual(atomicHistogram, atomicHistogram.copy());
-
-        ConcurrentHistogram concurrentHistogram = new ConcurrentHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        concurrentHistogram.recordValue(testValueLevel);
-        concurrentHistogram.recordValue(testValueLevel * 10);
-        concurrentHistogram.recordValueWithExpectedInterval(concurrentHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of scaled ConcurrentHistogram:");
-        assertEqual(concurrentHistogram, concurrentHistogram.copy());
-
-        SynchronizedHistogram syncHistogram = new SynchronizedHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        syncHistogram.recordValue(testValueLevel);
-        syncHistogram.recordValue(testValueLevel * 10);
-        syncHistogram.recordValueWithExpectedInterval(syncHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copy of scaled SynchronizedHistogram:");
-        assertEqual(syncHistogram, syncHistogram.copy());
     }
 
-    @Test
-    public void testCopyInto() throws Exception {
-        Histogram histogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
-        Histogram targetHistogram = new Histogram(highestTrackableValue, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testCopyInto(Class histoClass) throws Exception {
+        AbstractHistogram histogram =
+                constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
+        AbstractHistogram targetHistogram =
+                constructHistogram(histoClass, highestTrackableValue, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         histogram.recordValue(testValueLevel * 10);
         histogram.recordValueWithExpectedInterval(histogram.getHighestTrackableValue() - 1, 31000);
@@ -906,92 +1267,24 @@ public class HistogramTest {
 
         histogram.copyInto(targetHistogram);
         assertEqual(histogram, targetHistogram);
-
-
-        IntCountsHistogram intCountsHistogram = new IntCountsHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        IntCountsHistogram targetIntCountsHistogram = new IntCountsHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        intCountsHistogram.recordValue(testValueLevel);
-        intCountsHistogram.recordValue(testValueLevel * 10);
-        intCountsHistogram.recordValueWithExpectedInterval(intCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for IntHistogram:");
-        intCountsHistogram.copyInto(targetIntCountsHistogram);
-        assertEqual(intCountsHistogram, targetIntCountsHistogram);
-
-        intCountsHistogram.recordValue(testValueLevel * 20);
-
-        intCountsHistogram.copyInto(targetIntCountsHistogram);
-        assertEqual(intCountsHistogram, targetIntCountsHistogram);
-
-
-        ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        ShortCountsHistogram targetShortCountsHistogram = new ShortCountsHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        shortCountsHistogram.recordValue(testValueLevel);
-        shortCountsHistogram.recordValue(testValueLevel * 10);
-        shortCountsHistogram.recordValueWithExpectedInterval(shortCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for ShortHistogram:");
-        shortCountsHistogram.copyInto(targetShortCountsHistogram);
-        assertEqual(shortCountsHistogram, targetShortCountsHistogram);
-
-        shortCountsHistogram.recordValue(testValueLevel * 20);
-
-        shortCountsHistogram.copyInto(targetShortCountsHistogram);
-        assertEqual(shortCountsHistogram, targetShortCountsHistogram);
-
-
-        AtomicHistogram atomicHistogram = new AtomicHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        AtomicHistogram targetAtomicHistogram = new AtomicHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        atomicHistogram.recordValue(testValueLevel);
-        atomicHistogram.recordValue(testValueLevel * 10);
-        atomicHistogram.recordValueWithExpectedInterval(atomicHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for AtomicHistogram:");
-        atomicHistogram.copyInto(targetAtomicHistogram);
-        assertEqual(atomicHistogram, targetAtomicHistogram);
-
-        atomicHistogram.recordValue(testValueLevel * 20);
-
-        atomicHistogram.copyInto(targetAtomicHistogram);
-        assertEqual(atomicHistogram, targetAtomicHistogram);
-
-
-        ConcurrentHistogram concurrentHistogram = new ConcurrentHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        ConcurrentHistogram targetConcurrentHistogram = new ConcurrentHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        concurrentHistogram.recordValue(testValueLevel);
-        concurrentHistogram.recordValue(testValueLevel * 10);
-        concurrentHistogram.recordValueWithExpectedInterval(concurrentHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for ConcurrentHistogram:");
-        concurrentHistogram.copyInto(targetConcurrentHistogram);
-        assertEqual(concurrentHistogram, targetConcurrentHistogram);
-
-        concurrentHistogram.recordValue(testValueLevel * 20);
-
-        concurrentHistogram.copyInto(targetConcurrentHistogram);
-        assertEqual(concurrentHistogram, targetConcurrentHistogram);
-
-
-        SynchronizedHistogram syncHistogram = new SynchronizedHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        SynchronizedHistogram targetSyncHistogram = new SynchronizedHistogram(highestTrackableValue, numberOfSignificantValueDigits);
-        syncHistogram.recordValue(testValueLevel);
-        syncHistogram.recordValue(testValueLevel * 10);
-        syncHistogram.recordValueWithExpectedInterval(syncHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for SynchronizedHistogram:");
-        syncHistogram.copyInto(targetSyncHistogram);
-        assertEqual(syncHistogram, targetSyncHistogram);
-
-        syncHistogram.recordValue(testValueLevel * 20);
-
-        syncHistogram.copyInto(targetSyncHistogram);
-        assertEqual(syncHistogram, targetSyncHistogram);
     }
 
-    @Test
-    public void testScaledCopyInto() throws Exception {
-        Histogram histogram = new Histogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        Histogram targetHistogram = new Histogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
+    @ParameterizedTest
+    @ValueSource(classes = {
+            Histogram.class,
+            ConcurrentHistogram.class,
+            AtomicHistogram.class,
+            SynchronizedHistogram.class,
+            PackedHistogram.class,
+            PackedConcurrentHistogram.class,
+            IntCountsHistogram.class,
+            ShortCountsHistogram.class,
+    })
+    public void testScaledCopyInto(Class histoClass) throws Exception {
+        AbstractHistogram histogram =
+                constructHistogram(histoClass, 1000, highestTrackableValue, numberOfSignificantValueDigits);
+        AbstractHistogram targetHistogram =
+                constructHistogram(histoClass, 1000, highestTrackableValue, numberOfSignificantValueDigits);
         histogram.recordValue(testValueLevel);
         histogram.recordValue(testValueLevel * 10);
         histogram.recordValueWithExpectedInterval(histogram.getHighestTrackableValue() - 1, 31000);
@@ -1004,86 +1297,6 @@ public class HistogramTest {
 
         histogram.copyInto(targetHistogram);
         assertEqual(histogram, targetHistogram);
-
-
-        IntCountsHistogram intCountsHistogram = new IntCountsHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        IntCountsHistogram targetIntCountsHistogram = new IntCountsHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        intCountsHistogram.recordValue(testValueLevel);
-        intCountsHistogram.recordValue(testValueLevel * 10);
-        intCountsHistogram.recordValueWithExpectedInterval(intCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for scaled IntHistogram:");
-        intCountsHistogram.copyInto(targetIntCountsHistogram);
-        assertEqual(intCountsHistogram, targetIntCountsHistogram);
-
-        intCountsHistogram.recordValue(testValueLevel * 20);
-
-        intCountsHistogram.copyInto(targetIntCountsHistogram);
-        assertEqual(intCountsHistogram, targetIntCountsHistogram);
-
-
-        ShortCountsHistogram shortCountsHistogram = new ShortCountsHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        ShortCountsHistogram targetShortCountsHistogram = new ShortCountsHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        shortCountsHistogram.recordValue(testValueLevel);
-        shortCountsHistogram.recordValue(testValueLevel * 10);
-        shortCountsHistogram.recordValueWithExpectedInterval(shortCountsHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for scaled ShortHistogram:");
-        shortCountsHistogram.copyInto(targetShortCountsHistogram);
-        assertEqual(shortCountsHistogram, targetShortCountsHistogram);
-
-        shortCountsHistogram.recordValue(testValueLevel * 20);
-
-        shortCountsHistogram.copyInto(targetShortCountsHistogram);
-        assertEqual(shortCountsHistogram, targetShortCountsHistogram);
-
-
-        AtomicHistogram atomicHistogram = new AtomicHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        AtomicHistogram targetAtomicHistogram = new AtomicHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        atomicHistogram.recordValue(testValueLevel);
-        atomicHistogram.recordValue(testValueLevel * 10);
-        atomicHistogram.recordValueWithExpectedInterval(atomicHistogram.getHighestTrackableValue() - 1, 31000);
-
-        atomicHistogram.copyInto(targetAtomicHistogram);
-        assertEqual(atomicHistogram, targetAtomicHistogram);
-
-        atomicHistogram.recordValue(testValueLevel * 20);
-
-        System.out.println("Testing copyInto for scaled AtomicHistogram:");
-        atomicHistogram.copyInto(targetAtomicHistogram);
-        assertEqual(atomicHistogram, targetAtomicHistogram);
-
-
-        ConcurrentHistogram concurrentHistogram = new ConcurrentHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        ConcurrentHistogram targetConcurrentHistogram = new ConcurrentHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        concurrentHistogram.recordValue(testValueLevel);
-        concurrentHistogram.recordValue(testValueLevel * 10);
-        concurrentHistogram.recordValueWithExpectedInterval(concurrentHistogram.getHighestTrackableValue() - 1, 31000);
-
-        concurrentHistogram.copyInto(targetConcurrentHistogram);
-        assertEqual(concurrentHistogram, targetConcurrentHistogram);
-
-        concurrentHistogram.recordValue(testValueLevel * 20);
-
-        System.out.println("Testing copyInto for scaled ConcurrentHistogram:");
-        concurrentHistogram.copyInto(targetConcurrentHistogram);
-        assertEqual(concurrentHistogram, targetConcurrentHistogram);
-
-
-        SynchronizedHistogram syncHistogram = new SynchronizedHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        SynchronizedHistogram targetSyncHistogram = new SynchronizedHistogram(1000, highestTrackableValue, numberOfSignificantValueDigits);
-        syncHistogram.recordValue(testValueLevel);
-        syncHistogram.recordValue(testValueLevel * 10);
-        syncHistogram.recordValueWithExpectedInterval(syncHistogram.getHighestTrackableValue() - 1, 31000);
-
-        System.out.println("Testing copyInto for scaled SynchronizedHistogram:");
-        syncHistogram.copyInto(targetSyncHistogram);
-        assertEqual(syncHistogram, targetSyncHistogram);
-
-        syncHistogram.recordValue(testValueLevel * 20);
-
-        syncHistogram.copyInto(targetSyncHistogram);
-        assertEqual(syncHistogram, targetSyncHistogram);
     }
 
     public void verifyMaxValue(AbstractHistogram histogram) {
@@ -1096,4 +1309,5 @@ public class HistogramTest {
         computedMaxValue = (computedMaxValue == 0) ? 0 : histogram.highestEquivalentValue(computedMaxValue);
         Assert.assertEquals(computedMaxValue, histogram.getMaxValue());
     }
+
 }
diff --git a/src/test/java/org/HdrHistogram/HistogramTestUtils.java b/src/test/java/org/HdrHistogram/HistogramTestUtils.java
new file mode 100644
index 0000000..038aa37
--- /dev/null
+++ b/src/test/java/org/HdrHistogram/HistogramTestUtils.java
@@ -0,0 +1,105 @@
+package org.HdrHistogram;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+public class HistogramTestUtils {
+    static AbstractHistogram constructHistogram(Class c, Object... constructorArgs) {
+        try {
+            Class[] argTypes;
+            if (constructorArgs.length == 1) {
+                if (constructorArgs[0] instanceof AbstractHistogram) {
+                    argTypes = new Class[]{AbstractHistogram.class};
+                } else {
+                    argTypes = new Class[]{int.class};
+                }
+            } else if (constructorArgs.length == 2) {
+                argTypes = new Class[]{long.class, int.class};
+            } else if (constructorArgs.length == 3) {
+                argTypes = new Class[]{long.class, long.class, int.class};
+            } else {
+                throw new RuntimeException("Not an expected signature for Histogram constructor");
+            }
+            return (AbstractHistogram) c.getConstructor(argTypes).newInstance(constructorArgs);
+        } catch (InvocationTargetException ex) {
+            if (ex.getTargetException() instanceof IllegalArgumentException) {
+                throw new IllegalArgumentException(ex.getTargetException().getMessage(), ex);
+            } else {
+                throw new RuntimeException("Re-throwing: ", ex);
+            }
+        } catch (NoSuchMethodException | InstantiationException |
+                IllegalAccessException  ex) {
+            throw new RuntimeException("Re-throwing: ", ex);
+        }
+    }
+
+    static AbstractHistogram decodeFromCompressedByteBuffer(Class c,
+                                                            final ByteBuffer buffer,
+                                                            final long minBarForHighestTrackableValue) {
+        try {
+            Class[] argTypes = {ByteBuffer.class, long.class};
+            Method m = c.getMethod("decodeFromCompressedByteBuffer", argTypes);
+            return (AbstractHistogram) m.invoke(null, buffer, minBarForHighestTrackableValue);
+        } catch (InvocationTargetException ex) {
+            if (ex.getTargetException() instanceof IllegalArgumentException) {
+                throw new IllegalArgumentException(ex.getTargetException().getMessage(), ex);
+            } else {
+                throw new RuntimeException("Re-throwing: ", ex);
+            }
+        } catch (NoSuchMethodException | IllegalAccessException  ex) {
+            throw new RuntimeException("Re-throwing: ", ex);
+        }
+    }
+
+    static DoubleHistogram constructDoubleHistogram(Class c, Object... constructorArgs) {
+        try {
+            Class[] argTypes;
+            if (constructorArgs.length == 1) {
+                if (constructorArgs[0] instanceof DoubleHistogram) {
+                    argTypes = new Class[]{DoubleHistogram.class};
+                } else {
+                    argTypes = new Class[]{int.class};
+                }
+            } else if (constructorArgs.length == 2) {
+                if (constructorArgs[1] instanceof Class) {
+                    argTypes = new Class[]{int.class, Class.class};
+                } else {
+                    argTypes = new Class[]{long.class, int.class};
+                }
+            } else if (constructorArgs.length == 3) {
+                argTypes = new Class[]{long.class, int.class, Class.class};
+            } else {
+                throw new RuntimeException("Not an expected signature for DoubleHistogram constructor");
+            }
+            return (DoubleHistogram) c.getDeclaredConstructor(argTypes).newInstance(constructorArgs);
+        } catch (InvocationTargetException ex) {
+            if (ex.getTargetException() instanceof IllegalArgumentException) {
+                throw new IllegalArgumentException(ex.getTargetException().getMessage(), ex);
+            } else {
+                throw new RuntimeException("Re-throwing: ", ex);
+            }
+        } catch (NoSuchMethodException | InstantiationException |
+                IllegalAccessException  ex) {
+            throw new RuntimeException("Re-throwing: ", ex);
+        }
+    }
+
+    static DoubleHistogram decodeDoubleHistogramFromCompressedByteBuffer(Class c,
+                                                            final ByteBuffer buffer,
+                                                            final long minBarForHighestTrackableValue) {
+        try {
+            Class[] argTypes = {ByteBuffer.class, long.class};
+            Method m = c.getMethod("decodeFromCompressedByteBuffer", argTypes);
+            return (DoubleHistogram) m.invoke(null, buffer, minBarForHighestTrackableValue);
+        } catch (InvocationTargetException ex) {
+            if (ex.getTargetException() instanceof IllegalArgumentException) {
+                throw new IllegalArgumentException(ex.getTargetException().getMessage(), ex);
+            } else {
+                throw new RuntimeException("Re-throwing: ", ex);
+            }
+        } catch (NoSuchMethodException | IllegalAccessException  ex) {
+            throw new RuntimeException("Re-throwing: ", ex);
+        }
+    }
+}
diff --git a/src/test/java/org/HdrHistogram/RecorderTest.java b/src/test/java/org/HdrHistogram/RecorderTest.java
index 9e59a86..60cc334 100644
--- a/src/test/java/org/HdrHistogram/RecorderTest.java
+++ b/src/test/java/org/HdrHistogram/RecorderTest.java
@@ -8,29 +8,39 @@
 
 package org.HdrHistogram;
 
+import org.HdrHistogram.packedarray.PackedArrayRecorder;
+import org.HdrHistogram.packedarray.PackedLongArray;
 import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * JUnit test for {@link Histogram}
  */
 public class RecorderTest {
     static final long highestTrackableValue = 3600L * 1000 * 1000; // e.g. for 1 hr in usec units
+    static final int packedArrayLength = 300 * 10000 * 2;
+    static final int nonPackedPhysicalLength = 128 * 1024;
 
-    @Test
-    public void testIntervalRecording() throws Exception {
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testIntervalRecording(boolean usePacked) throws Exception {
 
         Histogram histogram = new Histogram(highestTrackableValue, 3);
         DoubleHistogram doubleHistogram = new DoubleHistogram(highestTrackableValue * 1000, 3);
         Recorder recorder1 =
-                new Recorder(highestTrackableValue, 3);
+                new Recorder(3, usePacked);
         Recorder recorder2 =
-                new Recorder(highestTrackableValue, 3);
+                new Recorder(3, usePacked);
         DoubleRecorder doubleRecorder1 =
-                new DoubleRecorder(highestTrackableValue * 1000, 3);
+                new DoubleRecorder(3, usePacked);
         DoubleRecorder doubleRecorder2 =
-                new DoubleRecorder(highestTrackableValue * 1000, 3);
-
+                new DoubleRecorder(3, usePacked);
+        PackedLongArray array1 = new PackedLongArray(packedArrayLength);
+        PackedArrayRecorder arrayRecorder1 = new PackedArrayRecorder(packedArrayLength);
 
         for (int i = 0; i < 10000; i++) {
             histogram.recordValue(3000 * i);
@@ -42,6 +52,8 @@ public class RecorderTest {
             doubleHistogram.recordValue(0.001); // Makes some internal shifts happen.
             doubleRecorder1.recordValue(0.001); // Makes some internal shifts happen.
             doubleRecorder2.recordValue(0.001); // Makes some internal shifts happen.
+            array1.increment(300 * i);
+            arrayRecorder1.increment(300 * i);
         }
 
         Histogram histogram2 = recorder1.getIntervalHistogram();
@@ -56,6 +68,10 @@ public class RecorderTest {
         doubleRecorder2.getIntervalHistogramInto(doubleHistogram2);
         Assert.assertEquals(doubleHistogram, doubleHistogram2);
 
+        PackedLongArray array2 = arrayRecorder1.getIntervalArray();
+        boolean arraysAreEqual = array1.equals(array2);
+        Assert.assertEquals(arraysAreEqual, true);
+
         for (int i = 0; i < 5000; i++) {
             histogram.recordValue(3000 * i);
             recorder1.recordValue(3000 * i);
@@ -66,6 +82,8 @@ public class RecorderTest {
             doubleHistogram.recordValue(0.001);
             doubleRecorder1.recordValue(0.001);
             doubleRecorder2.recordValue(0.001);
+            array1.increment(300 * i);
+            arrayRecorder1.increment(300 * i);
         }
 
         Histogram histogram3 = recorder1.getIntervalHistogram();
@@ -80,6 +98,13 @@ public class RecorderTest {
         sumDoubleHistogram.add(doubleHistogram3);
         Assert.assertEquals(doubleHistogram, sumDoubleHistogram);
 
+        PackedLongArray array3 = arrayRecorder1.getIntervalArray();
+
+        PackedLongArray sumArray = array2.copy();
+        sumArray.add(array3);
+        arraysAreEqual = array1.equals(sumArray);
+        Assert.assertEquals(arraysAreEqual, true);
+
         recorder2.getIntervalHistogram();
         doubleRecorder2.getIntervalHistogram();
 
@@ -93,6 +118,8 @@ public class RecorderTest {
             doubleHistogram.recordValue(0.001);
             doubleRecorder1.recordValue(0.001);
             doubleRecorder2.recordValue(0.001);
+            array1.increment(300 * i);
+            arrayRecorder1.increment(300 * i);
         }
 
         Histogram histogram4 = recorder1.getIntervalHistogram();
@@ -111,142 +138,261 @@ public class RecorderTest {
         doubleRecorder2.getIntervalHistogramInto(doubleHistogram4);
         doubleHistogram4.add(doubleHistogram3);
         Assert.assertEquals(doubleHistogram4, doubleHistogram2);
+
+        PackedLongArray array4 = arrayRecorder1.getIntervalArray();
+        array4.add(array3);
+        arraysAreEqual = array4.equals(array2);
+        Assert.assertEquals(arraysAreEqual, true);
     }
 
-    @Test
-    public void testSimpleAutosizingRecorder() throws Exception {
-        Recorder recorder = new Recorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSimpleAutosizingRecorder(boolean usePacked) throws Exception {
+        Recorder recorder = new Recorder(3, usePacked);
         Histogram histogram = recorder.getIntervalHistogram();
     }
 
+    // PackedArrayRecorder recycling tests:
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testPARecycling(boolean usePacked) throws Exception {
+        PackedArrayRecorder recorder = new PackedArrayRecorder(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+        PackedLongArray arrayA = recorder.getIntervalArray();
+        PackedLongArray arrayB = recorder.getIntervalArray(arrayA);
+        PackedLongArray arrayC = recorder.getIntervalArray(arrayB, true);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testPARecyclingContainingClassEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        PackedLongArray arrayToRecycle = new PackedLongArray(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+                        PackedArrayRecorder recorder = new PackedArrayRecorder(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+                        PackedLongArray arrayA = recorder.getIntervalArray(arrayToRecycle);
+                    }
+                });
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testPARecyclingContainingInstanceEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        PackedArrayRecorder recorder1 = new PackedArrayRecorder(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+                        PackedArrayRecorder recorder2 = new PackedArrayRecorder(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+                        PackedLongArray arrayToRecycle = recorder1.getIntervalArray();
+                        PackedLongArray arrayToRecycle2 = recorder2.getIntervalArray(arrayToRecycle);
+                    }
+                });
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testPARecyclingContainingInstanceNonEnforcement(final boolean usePacked) throws Exception {
+        PackedArrayRecorder recorder1 = new PackedArrayRecorder(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+        PackedArrayRecorder recorder2 = new PackedArrayRecorder(packedArrayLength, usePacked ? 0 : nonPackedPhysicalLength);
+        PackedLongArray arrayToRecycle = recorder1.getIntervalArray();
+        PackedLongArray arrayToRecycle2 = recorder2.getIntervalArray(arrayToRecycle, false);
+    }
+
     // Recorder Recycling tests:
 
-    @Test
-    public void testRecycling() throws Exception {
-        Recorder recorder = new Recorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testRecycling(boolean usePacked) throws Exception {
+        Recorder recorder = new Recorder(3, usePacked);
         Histogram histogramA = recorder.getIntervalHistogram();
         Histogram histogramB = recorder.getIntervalHistogram(histogramA);
-        Histogram histogramC = recorder.getIntervalHistogram(histogramA, true);
+        Histogram histogramC = recorder.getIntervalHistogram(histogramB, true);
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testRecyclingContainingClassEnforcement() throws Exception {
-        Histogram histToRecycle = new Histogram(3);
-        Recorder recorder = new Recorder(3);
-        Histogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testRecyclingContainingClassEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        Histogram histToRecycle = new Histogram(3);
+                        Recorder recorder = new Recorder(3, usePacked);
+                        Histogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testRecyclingContainingInstanceEnforcement() throws Exception {
-        Recorder recorder1 = new Recorder(3);
-        Recorder recorder2 = new Recorder(3);
-        Histogram histToRecycle = recorder1.getIntervalHistogram();
-        Histogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testRecyclingContainingInstanceEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        Recorder recorder1 = new Recorder(3, usePacked);
+                        Recorder recorder2 = new Recorder(3, usePacked);
+                        Histogram histToRecycle = recorder1.getIntervalHistogram();
+                        Histogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test
-    public void testRecyclingContainingInstanceNonEnforcement() throws Exception {
-        Recorder recorder1 = new Recorder(3);
-        Recorder recorder2 = new Recorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testRecyclingContainingInstanceNonEnforcement(final boolean usePacked) throws Exception {
+        Recorder recorder1 = new Recorder(3, usePacked);
+        Recorder recorder2 = new Recorder(3, usePacked);
         Histogram histToRecycle = recorder1.getIntervalHistogram();
         Histogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle, false);
     }
 
     // SingleWriterRecorder Recycling tests:
 
-    @Test
-    public void testSWRecycling() throws Exception {
-        SingleWriterRecorder recorder = new SingleWriterRecorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWRecycling(final boolean usePacked) throws Exception {
+        SingleWriterRecorder recorder = new SingleWriterRecorder(3, usePacked);
         Histogram histogramA = recorder.getIntervalHistogram();
         Histogram histogramB = recorder.getIntervalHistogram(histogramA);
-        Histogram histogramC = recorder.getIntervalHistogram(histogramA, true);
+        Histogram histogramC = recorder.getIntervalHistogram(histogramB, true);
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testSWRecyclingContainingClassEnforcement() throws Exception {
-        Histogram histToRecycle = new Histogram(3);
-        SingleWriterRecorder recorder = new SingleWriterRecorder(3);
-        Histogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWRecyclingContainingClassEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        Histogram histToRecycle = new Histogram(3);
+                        SingleWriterRecorder recorder = new SingleWriterRecorder(3, usePacked);
+                        Histogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testSWRecyclingContainingInstanceEnforcement() throws Exception {
-        SingleWriterRecorder recorder1 = new SingleWriterRecorder(3);
-        SingleWriterRecorder recorder2 = new SingleWriterRecorder(3);
-        Histogram histToRecycle = recorder1.getIntervalHistogram();
-        Histogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWRecyclingContainingInstanceEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        SingleWriterRecorder recorder1 = new SingleWriterRecorder(3, usePacked);
+                        SingleWriterRecorder recorder2 = new SingleWriterRecorder(3, usePacked);
+                        Histogram histToRecycle = recorder1.getIntervalHistogram();
+                        Histogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test
-    public void testSWRecyclingContainingInstanceNonEnforcement() throws Exception {
-        SingleWriterRecorder recorder1 = new SingleWriterRecorder(3);
-        SingleWriterRecorder recorder2 = new SingleWriterRecorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWRecyclingContainingInstanceNonEnforcement(final boolean usePacked) throws Exception {
+        SingleWriterRecorder recorder1 = new SingleWriterRecorder(3, usePacked);
+        SingleWriterRecorder recorder2 = new SingleWriterRecorder(3, usePacked);
         Histogram histToRecycle = recorder1.getIntervalHistogram();
         Histogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle, false);
     }
 
     // DoubleRecorder Recycling tests:
 
-    @Test
-    public void testDRecycling() throws Exception {
-        DoubleRecorder recorder = new DoubleRecorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testDRecycling(final boolean usePacked) throws Exception {
+        DoubleRecorder recorder = new DoubleRecorder(3, usePacked);
         DoubleHistogram histogramA = recorder.getIntervalHistogram();
         DoubleHistogram histogramB = recorder.getIntervalHistogram(histogramA);
-        DoubleHistogram histogramC = recorder.getIntervalHistogram(histogramA, true);
+        DoubleHistogram histogramC = recorder.getIntervalHistogram(histogramB, true);
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testDRecyclingContainingClassEnforcement() throws Exception {
-        DoubleHistogram histToRecycle = new DoubleHistogram(3);
-        DoubleRecorder recorder = new DoubleRecorder(3);
-        DoubleHistogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testDRecyclingContainingClassEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleHistogram histToRecycle = new DoubleHistogram(3);
+                        DoubleRecorder recorder = new DoubleRecorder(3, usePacked);
+                        DoubleHistogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testDRecyclingContainingInstanceEnforcement() throws Exception {
-        DoubleRecorder recorder1 = new DoubleRecorder(3);
-        DoubleRecorder recorder2 = new DoubleRecorder(3);
-        DoubleHistogram histToRecycle = recorder1.getIntervalHistogram();
-        DoubleHistogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testDRecyclingContainingInstanceEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleRecorder recorder1 = new DoubleRecorder(3, usePacked);
+                        DoubleRecorder recorder2 = new DoubleRecorder(3, usePacked);
+                        DoubleHistogram histToRecycle = recorder1.getIntervalHistogram();
+                        DoubleHistogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test
-    public void testDRecyclingContainingInstanceNonEnforcement() throws Exception {
-        DoubleRecorder recorder1 = new DoubleRecorder(3);
-        DoubleRecorder recorder2 = new DoubleRecorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testDRecyclingContainingInstanceNonEnforcement(final boolean usePacked) throws Exception {
+        DoubleRecorder recorder1 = new DoubleRecorder(3, usePacked);
+        DoubleRecorder recorder2 = new DoubleRecorder(3, usePacked);
         DoubleHistogram histToRecycle = recorder1.getIntervalHistogram();
         DoubleHistogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle, false);
     }
 
     // SingleWriterDoubleRecorder Recycling tests:
 
-    @Test
-    public void testSWDRecycling() throws Exception {
-        SingleWriterDoubleRecorder recorder = new SingleWriterDoubleRecorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWDRecycling(final boolean usePacked) throws Exception {
+        SingleWriterDoubleRecorder recorder = new SingleWriterDoubleRecorder(3, usePacked);
         DoubleHistogram histogramA = recorder.getIntervalHistogram();
         DoubleHistogram histogramB = recorder.getIntervalHistogram(histogramA);
-        DoubleHistogram histogramC = recorder.getIntervalHistogram(histogramA, true);
+        DoubleHistogram histogramC = recorder.getIntervalHistogram(histogramB, true);
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testSWDRecyclingContainingClassEnforcement() throws Exception {
-        DoubleHistogram histToRecycle = new DoubleHistogram(3);
-        SingleWriterDoubleRecorder recorder = new SingleWriterDoubleRecorder(3);
-        DoubleHistogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWDRecyclingContainingClassEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        DoubleHistogram histToRecycle = new DoubleHistogram(3);
+                        SingleWriterDoubleRecorder recorder = new SingleWriterDoubleRecorder(3, usePacked);
+                        DoubleHistogram histogramA = recorder.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testSWDRecyclingContainingInstanceEnforcement() throws Exception {
-        SingleWriterDoubleRecorder recorder1 = new SingleWriterDoubleRecorder(3);
-        SingleWriterDoubleRecorder recorder2 = new SingleWriterDoubleRecorder(3);
-        DoubleHistogram histToRecycle = recorder1.getIntervalHistogram();
-        DoubleHistogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWDRecyclingContainingInstanceEnforcement(final boolean usePacked) throws Exception {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                new Executable() {
+                    @Override
+                    public void execute() throws Throwable {
+                        SingleWriterDoubleRecorder recorder1 = new SingleWriterDoubleRecorder(3, usePacked);
+                        SingleWriterDoubleRecorder recorder2 = new SingleWriterDoubleRecorder(3, usePacked);
+                        DoubleHistogram histToRecycle = recorder1.getIntervalHistogram();
+                        DoubleHistogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle);
+                    }
+                });
     }
 
-    @Test
-    public void testSWDRecyclingContainingInstanceNonEnforcement() throws Exception {
-        SingleWriterDoubleRecorder recorder1 = new SingleWriterDoubleRecorder(3);
-        SingleWriterDoubleRecorder recorder2 = new SingleWriterDoubleRecorder(3);
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSWDRecyclingContainingInstanceNonEnforcement(final boolean usePacked) throws Exception {
+        SingleWriterDoubleRecorder recorder1 = new SingleWriterDoubleRecorder(3, usePacked);
+        SingleWriterDoubleRecorder recorder2 = new SingleWriterDoubleRecorder(3, usePacked);
         DoubleHistogram histToRecycle = recorder1.getIntervalHistogram();
         DoubleHistogram histToRecycle2 = recorder2.getIntervalHistogram(histToRecycle, false);
     }

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/maven-repo/org/hdrhistogram/HdrHistogram/2.1.12/HdrHistogram-2.1.12.pom
lrwxrwxrwx  root/root   /usr/share/java/hdrhistogram-2.1.12.jar -> hdrhistogram.jar
lrwxrwxrwx  root/root   /usr/share/maven-repo/org/hdrhistogram/HdrHistogram/2.1.12/HdrHistogram-2.1.12.jar -> ../../../../../java/hdrhistogram.jar

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/maven-repo/org/hdrhistogram/HdrHistogram/2.1.11/HdrHistogram-2.1.11.pom
lrwxrwxrwx  root/root   /usr/share/java/hdrhistogram-2.1.11.jar -> hdrhistogram.jar
lrwxrwxrwx  root/root   /usr/share/maven-repo/org/hdrhistogram/HdrHistogram/2.1.11/HdrHistogram-2.1.11.jar -> ../../../../../java/hdrhistogram.jar

No differences were encountered in the control files

More details

Full run details