Full Code of joyent/statemap for AI

master 04cc10f4c526 cached
30 files
342.1 KB
116.7k tokens
123 symbols
1 requests
Download .txt
Showing preview only (355K chars total). Download the full file or copy to clipboard to get everything.
Repository: joyent/statemap
Branch: master
Commit: 04cc10f4c526
Files: 30
Total size: 342.1 KB

Directory structure:
gitextract_6twupnbu/

├── .gitmodules
├── AUTHORS
├── Cargo.toml
├── LICENSE
├── README.md
├── contrib/
│   ├── cpu-statemap-tagged.d
│   ├── cpu-statemap.d
│   ├── io-statemap.d
│   ├── lx-cmd-statemap.d
│   ├── lx-statemap.d
│   ├── postgres-statemap.d
│   ├── postgres-zfs-statemap.d
│   ├── spa-sync-statemap.d
│   └── vdev-statemap.d
├── src/
│   ├── icons/
│   │   ├── LICENSE
│   │   └── README.md
│   ├── main.rs
│   ├── statemap-svg.css
│   ├── statemap-svg.defs
│   ├── statemap-svg.js
│   └── statemap.rs
└── tst/
    ├── tst.bad_line_basic.err
    ├── tst.bad_line_basic.in
    ├── tst.bad_line_newline.err
    ├── tst.bad_line_newline.in
    ├── tst.bad_line_whitespace.err
    ├── tst.bad_line_whitespace.in
    ├── tst.io.in
    ├── tst.tag_basic.in
    └── tst.tag_redefined.in

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitmodules
================================================
[submodule "deps/javascriptlint"]
	path = deps/javascriptlint
	url = git://github.com/davepacheco/javascriptlint.git
[submodule "deps/jsstyle"]
	path = deps/jsstyle
	url = git://github.com/davepacheco/jsstyle.git


================================================
FILE: AUTHORS
================================================
# Statemap authors, ordered by first contribution

Bryan Cantrill <bryan@joyent.com>
David Tolnay <dtolnay@gmail.com>



================================================
FILE: Cargo.toml
================================================
[package]
name = "statemap"
version = "0.1.0"
authors = ["Bryan Cantrill <bryan@joyent.com>"]

[dependencies]
getopts = "0.2"
memmap = "0.6"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
natord = "1.0.9"
palette = "0.4"
rand = "0.5"


================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================

1. Definitions
--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions
--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities
-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination
--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************
*                                                                      *
*  6. Disclaimer of Warranty                                           *
*  -------------------------                                           *
*                                                                      *
*  Covered Software is provided under this License on an "as is"       *
*  basis, without warranty of any kind, either expressed, implied, or  *
*  statutory, including, without limitation, warranties that the       *
*  Covered Software is free of defects, merchantable, fit for a        *
*  particular purpose or non-infringing. The entire risk as to the     *
*  quality and performance of the Covered Software is with You.        *
*  Should any Covered Software prove defective in any respect, You     *
*  (not any Contributor) assume the cost of any necessary servicing,   *
*  repair, or correction. This disclaimer of warranty constitutes an   *
*  essential part of this License. No use of any Covered Software is   *
*  authorized under this License except under this disclaimer.         *
*                                                                      *
************************************************************************

************************************************************************
*                                                                      *
*  7. Limitation of Liability                                          *
*  --------------------------                                          *
*                                                                      *
*  Under no circumstances and under no legal theory, whether tort      *
*  (including negligence), contract, or otherwise, shall any           *
*  Contributor, or anyone who distributes Covered Software as          *
*  permitted above, be liable to You for any direct, indirect,         *
*  special, incidental, or consequential damages of any character      *
*  including, without limitation, damages for lost profits, loss of    *
*  goodwill, work stoppage, computer failure or malfunction, or any    *
*  and all other commercial damages or losses, even if such party      *
*  shall have been informed of the possibility of such damages. This   *
*  limitation of liability shall not apply to liability for death or   *
*  personal injury resulting from such party's negligence to the       *
*  extent applicable law prohibits such limitation. Some               *
*  jurisdictions do not allow the exclusion or limitation of           *
*  incidental or consequential damages, so this exclusion and          *
*  limitation may not apply to You.                                    *
*                                                                      *
************************************************************************

8. Litigation
-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous
----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License
---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.


================================================
FILE: README.md
================================================
# Statemap

This repository contains the software for rendering _statemaps_, a
software visualization in which time is on the X axis and timelines
for discrete entities are stacked on the Y axis, with different states
for the discrete entities rendered in different colors.

Generating a statemap consists of two steps: *instrumentation* and
*rendering*.  The result is a SVG that can be visualized with a SVG 
viewer (e.g., a web browser), allowing *interaction*.

## Installation

To compile the command to render a statemap from instrumentation data:

    cargo build --release

Note that statemap requires Rust.

## Instrumentation

Statemaps themselves are methodology- and OS-agnostic, but instrumentation
is usually more system-specific.
The `contrib` directory contains instrumentation for specific methodologies
and systems that will generate data that can be used 
as input to the `statemap` command:

<table>
<tr>
<th>Name</th>
<th>Method</th>
<th>OS</th>
<th>Statemap description</th>
</tr>
<tr>
<td><a href="./contrib/cpu-statemap.d">cpu-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>CPU activity by CPU</td>
</tr>
<tr>
<td><a href="./contrib/cpu-statemap-tagged.d">cpu-statemap-tagged.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>CPU activity by CPU, tagged by origin of activity</td>
</tr>
<tr>
<td><a href="./contrib/io-statemap.d">io-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>SCSI devices in terms of number of outstanding I/O operations</td>
</tr>
<tr>
<td><a href="./contrib/lx-cmd-statemap.d">lx-cmd-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>Processes and threads of a specified command in an LX zone</td>
</tr>
<tr>
<td><a href="./contrib/lx-statemap.d">lx-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>Threads in a specified process in an LX zone</td>
</tr>
<tr>
<td><a href="./contrib/postgres-statemap.d">postgres-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>PostgreSQL processes</td>
</tr>
<tr>
<td><a href="./contrib/postgres-zfs-statemap.d">postgres-zfs-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>PostgreSQL processes, with ZFS-specific states</td>
</tr>
<tr>
<td><a href="./contrib/spa-sync-statemap.d">spa-sync-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>ZFS SPA sync thread state</td>
</tr>
<tr>
<td><a href="./contrib/vdev-statemap.d">vdev-statemap.d</a></td>
<td>DTrace</td>
<td>SmartOS</td>
<td>I/O activity by ZFS vdev</td>
</tr>
</table>

### Data format

To generate data for statemap generation,
instrumentation should create a file that consists of a stream of
concatenated JSON.
The expectation is that one JSON payload will consist of
metadata, with many JSON payloads containing data, but the metadata may
be split across multiple JSON payloads.  (No field can appear more than
once, however.)

#### Metadata

The following metadata fields are required: 

- `start`: A two-element array of integers consisting of the start time of
the data in seconds (the 0th element) and nanoseconds within the 
second (the 1st element).  The start time should be expressed in UTC.

- `states`: An object in which each member is the name of a valid
  entity state.  Each member object can contain the following :

  - `value`: The value by which this state will be referred to in the 
    data stream.

  - `color`: The color that should be used to render the state. If the
    color is not specified, a color will be selected at random.

  For example, here is a valid `states` object:

        "states": {
                "on-cpu": {"value": 0, "color": "#DAF7A6" },
                "off-cpu-waiting": {"value": 1, "color": "#f9f9f9" },
                "off-cpu-futex": {"value": 2, "color": "#f0f0f0" },
                "off-cpu-io": {"value": 3, "color": "#FFC300" },
                "off-cpu-blocked": {"value": 4, "color": "#C70039" },
                "off-cpu-dead": {"value": 5, "color": "#581845" }
        }
  
In addition, the metadata can contain the following optional fields are
optional:

- `title`: The title of the statemap, such that it can meaningfully be
  in the clause "statemap of `title` activity."

- `host`: The host on which the data was gathered.

#### Data

The data for a statemap is provided following the metadata as
concatenated JSON (that is, each JSON payload is a datum).  Each
datum is a JSON object that must contain the following members:

- `entity`: The name of the entity.

- `time`: The time of the datum, expressed as a nanosecond offset from
  the `start` member present in the metadata.

- `state`: The value of the state that begins at the time of the datum.

Each datum may also contain an additional member:

- `tag`: The tag for the state.  See State tagging, below.

#### State tagging

It is often helpful to examine additional dimensionality within a particular
state or states.  For example, in understanding CPU activity, it may be
helpful to understand not just that a CPU was in a state in which it was
executing a user thread, but the nature of the thread itself:  the thread
identifier, process identifier, process name, and so on.  To facilitate this,
statemaps support *state tagging* whereby an immutable tag is associated with a
particular transition to a particular state.  There can be an arbitrary
number of such tags, but the expectation is that there are many more state
transitions than there are tags.  Tags are indicated by the `tag` member of
the state datum payload.  Elsewhere in the stream of data (though not
necessarily before the tag is used), the tag should be defined with
a tag-defining JSON payload that contains the following two members:

- `tag`: A string that is the tag that is being defined.

- `state`: The state that corresponds to this tag.  Each `state`/`tag` tuple
  must have its own tag definition.

Beyond these two members, the tag definition can have any number of scalar
members.  Tags are immutable; if a tag is redefined, the last tag definition
will apply to all uses of that tag.  The tag should not contain member
definitions that would cause it to be ambiguous with respect to data (namely,
`entity` and `time` members).

As an example, here is a tag definition for a state that is associated with
interrupt activity that indicates the source device:

```
{ "state": 6, "tag": "ffffd0c4f8f52000", "driver": "mpt_sas", "instance": 1 }
```

And here is an example of a tagged state datum:

```
{ "time": "1579579142", "entity": "55", "state": 6, "tag": "ffffd0c4f8f52000" }
```

This would indicate that at time 1579579142, entity 55 went into state 6 --
and the tag for this state (in this case, the interrupting device) was
instance 1 of the `mpt_sas` driver.

## Rendering

To render a statemap, run the `statemap` command, providing an instrumentation
data file.  The resulting statemap will be written as a SVG on standard
output:

    statemap my-instrumentation-output.out > statemap.svg

Statemaps are interactive; the resulting SVG will contain controls that
enable it to be zoomed, panned, states selected, etc. (See Interaction,
below.)

By default, statemaps consist of all states for the entire time duration
represented in the input data.  Because there can be many, many states
represented in the input, states will (by default) be _coalesced_ when
the time spent in a state is deemed a sufficiently small fraction of
the overall time.  For a coalesced state, the statemap will track the
overall fraction of states present (and will use a color that represents
a proportional blend of those states' colors).  When a statemap contains
coalesced states, some information will be lost (namely, the exact
time delineations of state transitions within the coalesced state).
Coalesced states can be eliminated in one of two ways:  either the
state coalescence target can be increased via the `-c` option, or the
statemap can be regenerated to cover a smaller range of time with some
combination of the `-b` option (to denote a beginning time) and the `-d`
option (to denote a duration).  The number of coalesced states can be
determined by looking at the metadata placed at the end of of the output
SVG.

### Options

The `statemap` command has the following options:

- `-b` (`--begin`): Takes a time offset at which the statemap should begin.
The time offset may be expressed in floating point with an optional
suffix (e.g., `-b 12.719s`).

- `-c` (`--coalesce`): Specifies the coalescing factor. Higher numbers will
result in less coalescence.

- `-d` (`--duration`): Takes a duration time for the statemap.  The time
may be expressed in floating point with an optional suffix (e.g.,
`-d 491.2ms`).

- `-h` (`--state-height`): The height (in pixels) of each state in the
statemap.

- `-i` (`--ignore-tags`): Ignore tags in the input, acting as if each state
is untagged. (This will result in shorter run-time and a smaller resulting
SVG.)

- `-s` (`--sortby`): The state by which to sort (default is to sort by
entity).

- `-S` (`--stacksortby`): The state by which to sort statemaps, when
multiple like statemaps are stacked (default is for the statemaps to be in
the order specified).

## Interaction

A statemap has icons for zooming and panning.  As the statemap is zoomed,
the time labels on top of the X axis will be updatd to reflect the current
duration.

Clicking on a statemap will highlight both the time at the point of the
click as well as the state.  Zooming when a time is selected will center
the zoomed statemap at the specified time.  To clear the time, click on
the time label above the statemap; to select another time, simply click
on the statemap.

Shift-clicking (or Option-/Alt-clicking) on a statemap when a time is
highlighted will highlight a dotted line at the time selected, as well
as an indication of the time between the initial time highlighted and
the time selected.  This allows for the time delta between two events
to be easily ascertained.

## Stacked statemaps

To render a single SVG that contains multiple statemaps, multiple data
files can be provided:

    statemap data-1.out data-2.out > statemap.svg

The resulting statemaps will be stacked in the order of the data files as
provided on the command line, with the first data file dictating the time
bounds of the resulting stack.  The statemaps can be similar statemaps from
dissimilar entities (e.g., different machines), or they can be dissimilar
statemaps (e.g., different statemaps), or any mix of these.  Legends for
similar statemaps will be shared.  The `-S` option can control the sorting
of stacked like statemaps.

When statemaps are stacked, the coalescing factor applies to *each* statemap
rather than to the entire stack of statemaps.  When stacking many statemaps,
low coalescing factors will be needed to prevent the resulting SVG from
becoming excessively large.


================================================
FILE: contrib/cpu-statemap-tagged.d
================================================
#!/usr/sbin/dtrace -Cs

/*
 * Copyright 2018, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive
#pragma D option switchrate=500hz

#define STATE_UTHREAD	0
#define STATE_KTHREAD	1
#define	STATE_INTR	1
#define STATE_IDLE	17

#define T_INTR	1
inline int STATE_MAX = 17;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"CPU\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"entityKind\": \"CPU\",\n");
	printf("\t\"states\": {\n");

	/*
	 * Execution: shades of green
	 */
	STATE_METADATA(STATE_UTHREAD, "uthread", "#9BC362")
	STATE_METADATA(STATE_KTHREAD, "kthread", "#2E4E00")

	/*
	 * Low level interrupts: shades of aqua and then blue
	 */
	STATE_METADATA(STATE_INTR + 1, "level-1", "#689D99")
	STATE_METADATA(STATE_INTR + 2, "level-2", "#41837E")
	STATE_METADATA(STATE_INTR + 3, "level-3", "#236863")
	STATE_METADATA(STATE_INTR + 4, "level-4", "#0D4E4A")
	STATE_METADATA(STATE_INTR + 5, "level-5", "#003430")
	STATE_METADATA(STATE_INTR + 6, "level-6", "#817FB2")
	STATE_METADATA(STATE_INTR + 7, "level-7", "#575594")
	STATE_METADATA(STATE_INTR + 8, "level-8", "#363377")
	STATE_METADATA(STATE_INTR + 9, "level-9", "#1C1A59")
	STATE_METADATA(STATE_INTR + 10, "level-10", "#0A093B")

	/*
	 * High level interrupts: shades of red
	 */
	STATE_METADATA(STATE_INTR + 11, "level-11", "#FFAAAA")
	STATE_METADATA(STATE_INTR + 12, "level-12", "#D46A6A")
	STATE_METADATA(STATE_INTR + 13, "level-13", "#AA3939")
	STATE_METADATA(STATE_INTR + 14, "level-14", "#801515")
	STATE_METADATA(STATE_INTR + 15, "level-15", "#550000")

	STATE_METADATA(STATE_IDLE, "idle", "#e0e0e0")

	printf("\t}\n}\n");
	start = timestamp;
}

interrupt-start
/arg0 != NULL && !itagged[arg0]/
{
	itagged[arg0] = 1;

	this->pri = curthread->t_cpu->cpu_m.mcpu_pri;
	this->devi = (struct dev_info *)arg0;

	printf("{ \"state\": %d, \"tag\": \"%p\", ",
	    this->pri + STATE_INTR, arg0);
	printf("\"driver\": \"%s\", \"instance\": %d }\n",
	    stringof(`devnamesp[this->devi->devi_major].dn_name),
	    this->devi->devi_instance);
}

interrupt-start
/arg0 == NULL/
{
	this->pri = curthread->t_cpu->cpu_m.mcpu_pri;

	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id, this->pri + STATE_INTR);
}

interrupt-start
/arg0 != NULL/
{
	this->pri = curthread->t_cpu->cpu_m.mcpu_pri;

	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d, \"tag\": \"%p\" }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id, this->pri + STATE_INTR, arg0);

	/*
	 * We set this, but we don't bother to ever clear it:  the number of
	 * CPUs and number of interrupt levels are both finite and small.
	 */
	itag[curthread->t_cpu, this->pri] = arg0;
}

av_dispatch_softvect:entry
/!itagged[curthread->t_pil]/
{
	itagged[curthread->t_pil] = 1;

	printf("{ \"state\": %d, \"tag\": \"%p\", ", curthread->t_pil,
	    curthread->t_pil + STATE_INTR);
	printf("\"driver\": \"softint\", \"instance\": 0 }\n");
}

av_dispatch_softvect:entry
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d, \"tag\": \"%p\" }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id, curthread->t_pil + STATE_INTR,
	    curthread->t_pil);

	itag[curthread->t_cpu, curthread->t_pil] = curthread->t_pil;
}

interrupt-complete,
av_dispatch_softvect:return
{
	this->intr = curthread->t_intr;
	this->pri = curthread->t_cpu->cpu_m.mcpu_pri;
	this->idle = 0;
}

interrupt-complete,
av_dispatch_softvect:return
/(this->pri > 10 && curthread == curthread->t_cpu->cpu_idle_thread) ||
    (this->pri <= 10 && (this->intr == NULL ||
    this->intr == curthread->t_cpu->cpu_idle_thread))/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id, STATE_IDLE);
	this->idle = 1;
}

interrupt-complete,
av_dispatch_softvect:return
/!this->idle/
{
	/*
	 * We want to set the state (and tag) back to the thread that we're
	 * going to return to, which we do imperfectly in that we don't
	 * reflect high-level interrupts interrupting high-level interrupts
	 * (we will show this as the underlying thread executing).
	 */
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d, \"tag\": \"%p\" }\n",
	    timestamp - start, curthread->t_cpu->cpu_id, 
	    this->pri > 10 ?
	    (curthread->t_pil > 0 ? curthread->t_pil + STATE_INTR :
	    curthread->t_procp == &`p0 ? STATE_KTHREAD : STATE_UTHREAD) :
	    (this->intr->t_pil > 0 ? this->intr->t_pil + STATE_INTR :
	    this->intr->t_procp == &`p0 ? STATE_KTHREAD : STATE_UTHREAD),
	    this->pri > 10 ? 
	    (curthread->t_pil > 0 ? itag[curthread->t_cpu, curthread->t_pil] :
	    curthread->t_did) : 
	    (this->intr->t_pil > 0 ? itag[curthread->t_cpu, this->intr->t_pil] :
	    this->intr->t_did));
}

sched:::on-cpu
/curthread != curthread->t_cpu->cpu_idle_thread &&
    pid == 0 && !tagged[curthread->t_did]/
{
	tagged[curthread->t_did] = 1;

	printf("{ \"state\": %d, \"tag\": \"%p\", \"thread\": \"%a\", \"taskq\": \"%s\" }\n",
	    STATE_KTHREAD,
	    curthread->t_did, curthread->t_startpc,
	    curthread->t_taskq != NULL ?
	    stringof(((taskq_t *)curthread->t_taskq)->tq_name) : "<none>");
}

sched:::on-cpu
/curthread != curthread->t_cpu->cpu_idle_thread && pid != 0 &&
    !tagged[curthread->t_did]/
{
	tagged[curthread->t_did] = 1;

	/*
	 * The godforsaken strtok() mess is to deal with pr_psargs that
	 * contain an embedded quote, backslashing that quote to assure that
	 * we generate valid JSON.  Yes, it would be (MUCH!) easier if DTrace
	 * provided a subroutine to do this, and this is imperfect in that (1)
	 * it will eat backslashes and turn them into backslashed quotes, even
	 * if that's not correct and (2) it will elide the string with an
	 * ellipsis after the third quote or backslash -- but at least it will
	 * always yield parseable JSON!
	 */
	printf("{ \"state\": %d, \"tag\": \"%p\", \"pid\": \"%d\", \"tid\": \"%d\", \"execname\": \"%s\", \"psargs\": \"%s\" }\n",
	    STATE_UTHREAD, curthread->t_did, pid, tid,
	    execname, 
            strchr(curpsinfo->pr_psargs, '"') == NULL ? curpsinfo->pr_psargs :
            strjoin(strtok(curpsinfo->pr_psargs, "\"\\"),
            (this->s = strtok(NULL, "\"\\")) == NULL ? "" :
            strjoin(strjoin("\\\"", this->s),
            (this->s = strtok(NULL, "\"\\")) == NULL ? "" :
            strjoin(strjoin("\\\"", this->s),
            (this->s = strtok(NULL, "\"\\")) == NULL ? "" :
            strjoin(strjoin("\\\"", this->s),
            (this->s = strtok(NULL, "\"\\")) == NULL ? "" :
            strjoin("\\\"", this->s))))));
}

sched:::on-cpu
/curthread != curthread->t_cpu->cpu_idle_thread/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d, \"tag\": \"%p\" }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id,
	    (curthread->t_flag & T_INTR) ? curthread->t_pil + STATE_INTR :
	    (curthread->t_procp == &`p0 ? STATE_KTHREAD : STATE_UTHREAD),
	    (curthread->t_flag & T_INTR) ?
	    itag[curthread->t_cpu, curthread->t_pil] :
	    curthread->t_did);
}

sched:::on-cpu
/curthread == curthread->t_cpu->cpu_idle_thread/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id, STATE_IDLE);
}

tick-1sec
/timestamp - start > 10 * 1000000000/
{
	exit(0);
}



================================================
FILE: contrib/cpu-statemap.d
================================================
#!/usr/sbin/dtrace -Cs

/*
 * Copyright 2018, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive
#pragma D option switchrate=100hz

#define STATE_IDLE	0
#define STATE_UTHREAD	16
#define STATE_KTHREAD	17

#define T_INTR	1
inline int STATE_MAX = 17;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"CPU\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_IDLE, "idle", "#e0e0e0")

	/*
	 * Low level interrupts: shades of aqua and then blue
	 */
	STATE_METADATA(1, "level-1", "#689D99")
	STATE_METADATA(2, "level-2", "#41837E")
	STATE_METADATA(3, "level-3", "#236863")
	STATE_METADATA(4, "level-4", "#0D4E4A")
	STATE_METADATA(5, "level-5", "#003430")
	STATE_METADATA(6, "level-6", "#817FB2")
	STATE_METADATA(7, "level-7", "#575594")
	STATE_METADATA(8, "level-8", "#363377")
	STATE_METADATA(9, "level-9", "#1C1A59")
	STATE_METADATA(10, "level-10", "#0A093B")

	/*
	 * High level interrupts: shades of red
	 */
	STATE_METADATA(11, "level-11", "#FFAAAA")
	STATE_METADATA(12, "level-12", "#D46A6A")
	STATE_METADATA(13, "level-13", "#AA3939")
	STATE_METADATA(14, "level-14", "#801515")
	STATE_METADATA(15, "level-15", "#550000")

	/*
	 * Execution: shades of green
	 */
	STATE_METADATA(STATE_UTHREAD, "uthread", "#9BC362")
	STATE_METADATA(STATE_KTHREAD, "kthread", "#2E4E00")

	printf("\t}\n}\n");
	start = timestamp;
}

interrupt-start
{
	this->pri = curthread->t_cpu->cpu_m.mcpu_pri;

	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id, this->pri);
}

interrupt-complete
{
	this->intr = curthread->t_intr;
	this->pri = curthread->t_cpu->cpu_m.mcpu_pri;

	/*
	 * This is a bit gnarly: we need to set the state to be back to
	 * what it was before the interrupt took place.  This is slightly
	 * imperfect in that it doesn't quite reflect high-level interrupts
	 * interrupting high-level interrupts, but that should be an unusual
	 * enough condition that this should be good enough for most purposes.
	 */
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id,
	    this->pri > 10 ? 
	    (curthread == curthread->t_cpu->cpu_idle_thread ? STATE_IDLE :
	    curthread->t_pil > 0 ? curthread->t_pil :
	    curthread->t_procp == &`p0 ? STATE_KTHREAD : STATE_UTHREAD) :
	    this->intr != NULL ?
	    (this->intr == curthread->t_cpu->cpu_idle_thread ? STATE_IDLE :
	    this->intr->t_pil > 0 ? this->intr->t_pil :
	    this->intr->t_procp == &`p0 ? STATE_KTHREAD : STATE_UTHREAD) :
	    STATE_IDLE);
}

sched:::on-cpu
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", \"state\": %d }\n",
	    timestamp - start,
	    curthread->t_cpu->cpu_id,
	    curthread == curthread->t_cpu->cpu_idle_thread ? STATE_IDLE :
	    (curthread->t_flag & T_INTR) ? curthread->t_pil :
	    curthread->t_procp == &`p0 ? STATE_KTHREAD :
	    STATE_UTHREAD);
}

tick-1sec
/timestamp - start > 10 * 1000000000/
{
	exit(0);
}



================================================
FILE: contrib/io-statemap.d
================================================
#!/usr/sbin/dtrace -Cs

/*
 * Copyright 2018, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive

inline int STATE_MAXIO = 20;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAXIO ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"disk I/O\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"states\": {\n");

	STATE_METADATA(0, "no I/O", "#e0e0e0")
	STATE_METADATA(1, "1 I/O", "#DFE500");
	STATE_METADATA(2, "2 I/Os", "#DDD800");
	STATE_METADATA(3, "3 I/Os", "#DBCC01");
	STATE_METADATA(4, "4 I/Os", "#D9C002");
	STATE_METADATA(5, "5 I/Os", "#D8B403");
	STATE_METADATA(6, "6 I/Os", "#D6A804");
	STATE_METADATA(7, "7 I/Os", "#D49C05");
	STATE_METADATA(8, "8 I/Os", "#D39006");
	STATE_METADATA(9, "9 I/Os", "#D18407");
	STATE_METADATA(10, "10 I/Os", "#CF7808");
	STATE_METADATA(11, "11 I/Os", "#CE6C09");
	STATE_METADATA(12, "12 I/Os", "#CC600A");
	STATE_METADATA(13, "13 I/Os", "#CA540B");
	STATE_METADATA(14, "14 I/Os", "#C9480C");
	STATE_METADATA(15, "15 I/Os", "#C73C0D");
	STATE_METADATA(16, "16 I/Os", "#C5300E");
	STATE_METADATA(17, "17 I/Os", "#C4240F");
	STATE_METADATA(18, "18 I/Os", "#C21810");
	STATE_METADATA(19, "19 I/Os", "#C00C11");
	STATE_METADATA(STATE_MAXIO, ">=20 I/Os", "#BF0012");

	printf("\t}\n}\n");
	start = timestamp;
}

scsi-transport-dispatch
{
	this->b = (struct buf *)arg0;
	this->u = ((struct sd_xbuf *)this->b->b_private)->xb_un;

	printf("{ \"time\": \"%d\", \"entity\": \"sd%d\", \"state\": %d }\n",
	    timestamp - start,
	    ((struct dev_info *)this->u->un_sd->sd_dev)->devi_instance,
	    this->u->un_ncmds_in_transport < STATE_MAXIO ?
	    this->u->un_ncmds_in_transport : STATE_MAXIO);
}

sdintr:entry
{
	this->b = (struct buf *)args[0]->pkt_private;
	self->un = ((struct sd_xbuf *)this->b->b_private)->xb_un;
}

sdintr:return
/(this->u = self->un) != NULL/
{
	printf("{ \"time\": \"%d\", \"entity\": \"sd%d\", \"state\": %d }\n",
	    timestamp - start,
	    ((struct dev_info *)this->u->un_sd->sd_dev)->devi_instance,
	    this->u->un_ncmds_in_transport < STATE_MAXIO ?
	    this->u->un_ncmds_in_transport : STATE_MAXIO);

	self->un = NULL;
}

tick-1sec
/timestamp - start > 10 * 1000000000/
{
	exit(0);
}


================================================
FILE: contrib/lx-cmd-statemap.d
================================================
#!/usr/sbin/dtrace -Cs 

/*
 * Copyright 2017, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive

#define T_WAKEABLE	0x0002

typedef enum {
	STATE_ON_CPU = 0,
	STATE_OFF_CPU_WAITING = 1,
	STATE_OFF_CPU_FUTEX = 2,
	STATE_OFF_CPU_IO = 3,
	STATE_OFF_CPU_BLOCKED = 4,
	STATE_OFF_CPU_DEAD = 5,
	STATE_MAX = 6
} state_t;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX - 1 ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"all %s LX processes\",\n", $$1);
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_ON_CPU, "on-cpu", "#DAF7A6")
	STATE_METADATA(STATE_OFF_CPU_WAITING, "off-cpu-waiting", "#f9f9f9")
	STATE_METADATA(STATE_OFF_CPU_FUTEX, "off-cpu-futex", "#f0f0f0")
	STATE_METADATA(STATE_OFF_CPU_IO, "off-cpu-io", "#FFC300")
	STATE_METADATA(STATE_OFF_CPU_BLOCKED, "off-cpu-blocked", "#C70039")
	STATE_METADATA(STATE_OFF_CPU_DEAD, "off-cpu-dead", "#581845")

	printf("\t}\n}\n");
	start = timestamp;
}

zfs_fillpage:entry
/execname == $$1/
{
	self->state = STATE_OFF_CPU_IO;
}

zfs_fillpage:return
/execname == $$1/
{
	self->state = STATE_ON_CPU;
}

lx_futex:entry
/execname == $$1/
{
	self->state = STATE_OFF_CPU_FUTEX;
}

lx_futex:return
/execname == $$1/
{
	self->state = STATE_ON_CPU;
}

sched:::off-cpu
/execname == $$1/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d/%d\", ",
	    timestamp - start, pid, tid);

	printf("\"state\": %d }\n", self->state != STATE_ON_CPU ?
	    self->state : curthread->t_flag & T_WAKEABLE ?
	    STATE_OFF_CPU_WAITING : STATE_OFF_CPU_BLOCKED);
}

sched:::on-cpu
/execname == $$1/
{
	self->state = STATE_ON_CPU;
	printf("{ \"time\": \"%d\", \"entity\": \"%d/%d\", ",
	    timestamp - start, pid, tid);
	printf("\"state\": %d }\n", self->state);
}

proc:::lwp-exit
/execname == $$1/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d/%d\", ",
	    timestamp - start, pid, tid);
	printf("\"state\": %d }\n", STATE_OFF_CPU_DEAD);
}

tick-1sec
/timestamp - start > 60 * 1000000000/
{
	exit(0);
}


================================================
FILE: contrib/lx-statemap.d
================================================
#!/usr/sbin/dtrace -Cs 

/*
 * Copyright 2017, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive

#define T_WAKEABLE	0x0002

typedef enum {
	STATE_ON_CPU = 0,
	STATE_OFF_CPU_WAITING = 1,
	STATE_OFF_CPU_FUTEX = 2,
	STATE_OFF_CPU_IO = 3,
	STATE_OFF_CPU_BLOCKED = 4,
	STATE_OFF_CPU_DEAD = 5,
	STATE_MAX = 6
} state_t;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX - 1 ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"LX process ID %s\",\n", $$1);
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_ON_CPU, "on-cpu", "#DAF7A6")
	STATE_METADATA(STATE_OFF_CPU_WAITING, "off-cpu-waiting", "#f9f9f9")
	STATE_METADATA(STATE_OFF_CPU_FUTEX, "off-cpu-futex", "#f0f0f0")
	STATE_METADATA(STATE_OFF_CPU_IO, "off-cpu-io", "#FFC300")
	STATE_METADATA(STATE_OFF_CPU_BLOCKED, "off-cpu-blocked", "#C70039")
	STATE_METADATA(STATE_OFF_CPU_DEAD, "off-cpu-dead", "#581845")

	printf("\t}\n}\n");
	start = timestamp;
}

sched:::wakeup
/pid == $1 && args[1]->pr_pid == $1/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, tid);
	printf("\"event\": \"wakeup\", \"target\": \"%d\" }\n",
	    args[0]->pr_lwpid);
}

zfs_fillpage:entry
/pid == $1/
{
	self->state = STATE_OFF_CPU_IO;
}

zfs_fillpage:return
/pid == $1/
{
	self->state = STATE_ON_CPU;
}

lx_futex:entry
/pid == $1/
{
	self->state = STATE_OFF_CPU_FUTEX;
}

lx_futex:return
/pid == $1/
{
	self->state = STATE_ON_CPU;
}

sched:::off-cpu
/pid == $1/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, tid);

	printf("\"state\": %d }\n", self->state != STATE_ON_CPU ?
	    self->state : curthread->t_flag & T_WAKEABLE ?
	    STATE_OFF_CPU_WAITING : STATE_OFF_CPU_BLOCKED);
}

sched:::on-cpu
/pid == $1/
{
	self->state = STATE_ON_CPU;
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, tid);
	printf("\"state\": %d }\n", self->state);
}

proc:::lwp-exit
/pid == $1/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, tid);
	printf("\"state\": %d }\n", STATE_OFF_CPU_DEAD);
}

tick-1sec
/timestamp - start > 60 * 1000000000/
{
	exit(0);
}


================================================
FILE: contrib/postgres-statemap.d
================================================
#!/usr/sbin/dtrace -Cs 

/*
 * Copyright 2017, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive

#define T_WAKEABLE	0x0002

typedef enum {
	STATE_ON_CPU = 0,
	STATE_OFF_CPU_WAITING,
	STATE_OFF_CPU_SEMOP,
	STATE_OFF_CPU_BLOCKED,
	STATE_OFF_CPU_IO_READ,
	STATE_OFF_CPU_IO_WRITE,
	STATE_OFF_CPU_DEAD,
	STATE_MAX
} state_t;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX - 1 ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"PostgreSQL\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"entityKind\": \"Process\",\n");
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_ON_CPU, "on-cpu", "#DAF7A6")
	STATE_METADATA(STATE_OFF_CPU_WAITING, "off-cpu-waiting", "#f9f9f9")
	STATE_METADATA(STATE_OFF_CPU_SEMOP, "off-cpu-semop", "#FF5733")
	STATE_METADATA(STATE_OFF_CPU_BLOCKED, "off-cpu-blocked", "#C70039")
	STATE_METADATA(STATE_OFF_CPU_IO_READ, "off-cpu-io-read", "#FFC300")
	STATE_METADATA(STATE_OFF_CPU_IO_WRITE, "off-cpu-io-write", "#338AFF")
	STATE_METADATA(STATE_OFF_CPU_DEAD, "off-cpu-dead", "#E0E0E0")

	printf("\t}\n}\n");
	start = timestamp;
}

sched:::wakeup
/execname == "postgres" && args[1]->pr_fname == "postgres"/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, pid);
	printf("\"event\": \"wakeup\", \"target\": \"%d\" }\n",
	    args[1]->pr_pid);
}

syscall::read:entry
/execname == "postgres"/
{
	self->state = STATE_OFF_CPU_IO_READ;
}

syscall::write:entry
/execname == "postgres"/
{
	self->state = STATE_OFF_CPU_IO_WRITE;
}

syscall::read:return,
syscall::write:return
/execname == "postgres"/
{
	self->state = STATE_ON_CPU;
}

fbt::semop:entry
/execname == "postgres"/
{
	self->state = STATE_OFF_CPU_SEMOP;
}

fbt::semop:return
/execname == "postgres"/
{
	self->state = STATE_ON_CPU;
}

sched:::off-cpu
/execname == "postgres"/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, pid);

	printf("\"state\": %d }\n", self->state != STATE_ON_CPU ?
	    self->state : curthread->t_flag & T_WAKEABLE ?
	    STATE_OFF_CPU_WAITING : STATE_OFF_CPU_BLOCKED);
}

sched:::on-cpu
/execname == "postgres"/
{
	self->state = STATE_ON_CPU;
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, pid);
	printf("\"state\": %d }\n", self->state);
}

proc:::exit
/execname == "postgres"/
{
	self->exiting = pid;
}

sched:::off-cpu
/execname != "postgres" && self->exiting/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, self->exiting);

	printf("\"state\": %d }\n", STATE_OFF_CPU_DEAD);
	self->exiting = 0;
	self->state = 0;
}

/*
 * This is -- to put it mildly -- very specific to the implementation of
 * PostgreSQL: if the process is long-running, it lifts argv[0] out of the
 * address space, and -- iff it matches the form "postgres: [description]
 * process", sets the description for the process to be [description].
 */
sched:::on-cpu
/execname == "postgres" &&
    timestamp - curthread->t_procp->p_mstart > 1000000000 &&
    !seen[pid]/
{
	seen[pid] = 1;
	this->arg = *(uintptr_t *)copyin(curthread->t_procp->p_user.u_argv, 8);
	this->index = index(this->process = copyinstr(this->arg), " process");

	if (this->index > 0 && index(this->process, "postgres: ") == 0) {
		printf("{ \"entity\": \"%d\", \"description\": \"%s\" }\n",
		    pid, substr(this->process, 10, this->index - 10));
	}
}

tick-1sec
/timestamp - start > 60 * 1000000000/
{
	exit(0);
}


================================================
FILE: contrib/postgres-zfs-statemap.d
================================================
#!/usr/sbin/dtrace -Cs 

/*
 * Copyright 2018, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive

#define T_WAKEABLE	0x0002

typedef enum {
	STATE_ON_CPU = 0,
	STATE_OFF_CPU_WAITING,
	STATE_OFF_CPU_SEMOP,
	STATE_OFF_CPU_BLOCKED,
	STATE_OFF_CPU_ZFS_READ,
	STATE_OFF_CPU_ZFS_WRITE,
	STATE_OFF_CPU_ZIL_COMMIT,
	STATE_OFF_CPU_TX_DELAY,
	STATE_OFF_CPU_DEAD,
	STATE_MAX
} state_t;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX - 1 ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"PostgreSQL\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"entityKind\": \"Process\",\n");
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_ON_CPU, "on-cpu", "#DAF7A6")
	STATE_METADATA(STATE_OFF_CPU_WAITING, "off-cpu-waiting", "#f9f9f9")
	STATE_METADATA(STATE_OFF_CPU_SEMOP, "off-cpu-semop", "#FF5733")
	STATE_METADATA(STATE_OFF_CPU_BLOCKED, "off-cpu-blocked", "#C70039")
	STATE_METADATA(STATE_OFF_CPU_ZFS_READ, "off-cpu-zfs-read", "#FFC300")
	STATE_METADATA(STATE_OFF_CPU_ZFS_WRITE, "off-cpu-zfs-write", "#338AFF")
	STATE_METADATA(STATE_OFF_CPU_ZIL_COMMIT,
	    "off-cpu-zil-commit", "#66FFCC")
	STATE_METADATA(STATE_OFF_CPU_TX_DELAY, "off-cpu-tx-delay", "#CCFF00")
	STATE_METADATA(STATE_OFF_CPU_DEAD, "off-cpu-dead", "#E0E0E0")

	printf("\t}\n}\n");
	start = timestamp;
}

sched:::wakeup
/execname == "postgres" && args[1]->pr_fname == "postgres"/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, pid);
	printf("\"event\": \"wakeup\", \"target\": \"%d\" }\n",
	    args[1]->pr_pid);
}

fbt::zfs_read:entry
/execname == "postgres"/
{
	self->state = STATE_OFF_CPU_ZFS_READ;
}

fbt::zfs_write:entry
/execname == "postgres"/
{
	self->state = STATE_OFF_CPU_ZFS_WRITE;
}

syscall:::return
/execname == "postgres"/
{
	self->state = STATE_ON_CPU;
}

fbt::semop:entry
/execname == "postgres"/
{
	self->state = STATE_OFF_CPU_SEMOP;
}

fbt::semop:return
/execname == "postgres"/
{
	self->state = STATE_ON_CPU;
}

fbt::zil_commit:entry
/self->state == STATE_OFF_CPU_ZFS_WRITE/
{
	self->state = STATE_OFF_CPU_ZIL_COMMIT;
}

fbt::zil_commit:return
/self->state == STATE_OFF_CPU_ZIL_COMMIT/
{
	self->state = STATE_OFF_CPU_ZFS_WRITE;
}

fbt::dmu_tx_delay:entry
/self->state == STATE_OFF_CPU_ZFS_WRITE/
{
	self->state = STATE_OFF_CPU_TX_DELAY;
}

fbt::dmu_tx_delay:return
/self->state == STATE_OFF_CPU_TX_DELAY/
{
	self->state = STATE_OFF_CPU_ZFS_WRITE;
}

sched:::off-cpu
/execname == "postgres"/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, pid);

	printf("\"state\": %d }\n", self->state != STATE_ON_CPU ?
	    self->state : (curthread->t_flag & T_WAKEABLE ?
	    STATE_OFF_CPU_WAITING : STATE_OFF_CPU_BLOCKED));
}

sched:::on-cpu
/execname == "postgres"/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, pid);
	printf("\"state\": %d }\n", STATE_ON_CPU);
}

proc:::exit
/execname == "postgres"/
{
	self->exiting = pid;
}

sched:::off-cpu
/execname != "postgres" && self->exiting/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%d\", ",
	    timestamp - start, self->exiting);

	printf("\"state\": %d }\n", STATE_OFF_CPU_DEAD);
	self->exiting = 0;
	self->state = 0;
}

/*
 * This is -- to put it mildly -- very specific to the implementation of
 * PostgreSQL: if the process is long-running, it lifts argv[0] out of the
 * address space, and -- iff it matches the form "postgres: [description]
 * process", sets the description for the process to be [description].
 */
sched:::on-cpu
/execname == "postgres" &&
    timestamp - curthread->t_procp->p_mstart > 1000000000 &&
    !seen[pid]/
{
	seen[pid] = 1;
	this->arg = *(uintptr_t *)copyin(curthread->t_procp->p_user.u_argv, 8);
	this->index = index(this->process = copyinstr(this->arg), " process");

	if (this->index > 0 && index(this->process, "postgres: ") == 0) {
		printf("{ \"entity\": \"%d\", \"description\": \"%s\" }\n",
		    pid, substr(this->process, 10, this->index - 10));
	}
}

tick-1sec
/timestamp - start > 120 * 1000000000/
{
	exit(0);
}


================================================
FILE: contrib/spa-sync-statemap.d
================================================
#!/usr/sbin/dtrace -Cs 

/*
 * Copyright 2018, Joyent, Inc.
 */

#pragma D option quiet

typedef enum {
	STATE_ON_CPU = 0,
	STATE_OFF_CPU_WAITING,
	STATE_OFF_CPU_BLOCKED,
	STATE_OFF_CPU_ZIO_WAIT,
	STATE_OFF_CPU_ZIO_WAIT_MOS,
	STATE_OFF_CPU_ZIO_WAIT_SYNC,
	STATE_OFF_CPU_OBJSET_SYNC,
	STATE_OFF_CPU_CV,
	STATE_OFF_CPU_PREEMPTED,
	STATE_MAX
} state_t;

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX - 1 ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	printf("\t\"title\": \"SPA sync\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"entityKind\": \"SPA sync thread for pool\",\n");
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_ON_CPU, "on-cpu", "#DAF7A6")
	STATE_METADATA(STATE_OFF_CPU_WAITING, "off-cpu-waiting", "#f9f9f9")
	STATE_METADATA(STATE_OFF_CPU_BLOCKED, "off-cpu-blocked", "#C70039")
	STATE_METADATA(STATE_OFF_CPU_ZIO_WAIT, "off-cpu-zio-wait", "#FFC300")
	STATE_METADATA(STATE_OFF_CPU_ZIO_WAIT_MOS,
	    "off-cpu-zio-wait-mos", "#FF5733")
	STATE_METADATA(STATE_OFF_CPU_ZIO_WAIT_SYNC,
	    "off-cpu-zio-wait-sync", "#BB8FCE")
	STATE_METADATA(STATE_OFF_CPU_OBJSET_SYNC,
	    "off-cpu-objset-sync", "#338AFF")
	STATE_METADATA(STATE_OFF_CPU_CV, "off-cpu-cv", "#66FFCC")
	STATE_METADATA(STATE_OFF_CPU_PREEMPTED, "off-cpu-preempted", "#CCFF00")

	printf("\t}\n}\n");
	start = timestamp;
}

fbt::spa_sync:return
{
	self->state = STATE_OFF_CPU_WAITING;
}

fbt::spa_sync:entry
{
	self->spa = args[0];
	self->state = STATE_ON_CPU;
}

fbt::zio_wait:entry
/self->spa != NULL && self->state == STATE_ON_CPU/
{
	self->state = STATE_OFF_CPU_ZIO_WAIT;
	
}

fbt::zio_wait:return
/self->state == STATE_OFF_CPU_ZIO_WAIT/
{
	self->state = STATE_ON_CPU;
}

fbt::vdev_config_sync:entry
/self->state == STATE_ON_CPU/
{
	self->state = STATE_OFF_CPU_ZIO_WAIT_SYNC;
}

fbt::vdev_config_sync:return
/self->state == STATE_OFF_CPU_ZIO_WAIT_SYNC/
{
	self->state = STATE_ON_CPU;
}

fbt::dsl_pool_sync_mos:entry
/self->state == STATE_ON_CPU/
{
	self->state = STATE_OFF_CPU_ZIO_WAIT_MOS;
}

fbt::dsl_pool_sync_mos:return
/self->state == STATE_OFF_CPU_ZIO_WAIT_MOS/
{
	self->state = STATE_ON_CPU;
}

fbt::dmu_objset_sync:entry
/self->state == STATE_ON_CPU/
{
	self->state = STATE_OFF_CPU_OBJSET_SYNC;
}

fbt::dmu_objset_sync:return
/self->state == STATE_OFF_CPU_OBJSET_SYNC/
{
	self->state = STATE_ON_CPU;
}

sched:::off-cpu
/self->spa != NULL/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%s\", ",
	    timestamp - start, self->spa->spa_name);

	printf("\"state\": %d }\n",
	    self->state != STATE_ON_CPU ? self->state : 
	    curthread->t_sobj_ops == NULL ? STATE_OFF_CPU_PREEMPTED :
	    curthread->t_sobj_ops == &`cv_sobj_ops ? STATE_OFF_CPU_CV :
	    STATE_OFF_CPU_BLOCKED);
}

sched:::on-cpu
/self->spa != NULL/
{
	printf("{ \"time\": \"%d\", \"entity\": \"%s\", ",
	    timestamp - start, self->spa->spa_name);
	printf("\"state\": %d }\n", STATE_ON_CPU);
}

tick-1sec
/timestamp - start > 300 * 1000000000/
{
	exit(0);
}



================================================
FILE: contrib/vdev-statemap.d
================================================
#!/usr/sbin/dtrace -Cs

/*
 * Copyright 2018, Joyent, Inc.
 */

#pragma D option quiet
#pragma D option destructive

typedef enum zio_priority {
	ZIO_PRIORITY_SYNC_READ,
	ZIO_PRIORITY_SYNC_WRITE,        /* ZIL */
	ZIO_PRIORITY_ASYNC_READ,        /* prefetch */
	ZIO_PRIORITY_ASYNC_WRITE,       /* spa_sync() */
	ZIO_PRIORITY_SCRUB,             /* asynchronous scrub/resilver reads */
	ZIO_PRIORITY_REMOVAL,           /* reads/writes for vdev removal */
	ZIO_PRIORITY_INITIALIZING,      /* initializing I/O */
	ZIO_PRIORITY_NUM_QUEUEABLE
} zio_priority_t;

typedef enum {
	STATE_NONE = 0,
	STATE_READ,
	STATE_WRITE,
	STATE_RW,
	STATE_MAX
} state_t;

state_t state[vdev_queue_t *];

#define STATE_METADATA(_state, _str, _color) \
	printf("\t\t\"%s\": {\"value\": %d, \"color\": \"%s\" }%s\n", \
	    _str, _state, _color, _state < STATE_MAX - 1 ? "," : "");

BEGIN
{
	wall = walltimestamp;
	printf("{\n\t\"start\": [ %d, %d ],\n",
	    wall / 1000000000, wall % 1000000000);
	
	printf("\t\"title\": \"vdev I/O\",\n");
	printf("\t\"host\": \"%s\",\n", `utsname.nodename);
	printf("\t\"states\": {\n");

	STATE_METADATA(STATE_NONE, "idle", "#e0e0e0");
	STATE_METADATA(STATE_READ, "reading", "#FFC300");
	STATE_METADATA(STATE_WRITE, "writing", "#FF5733");
	STATE_METADATA(STATE_RW, "reading+writing", "#C70039");

	printf("\t}\n}\n");
	start = timestamp;
}

vdev_queue_pending_add:entry
{
	this->prio = (args[1]->io_priority == ZIO_PRIORITY_SYNC_WRITE ||
	    args[1]->io_priority == ZIO_PRIORITY_ASYNC_WRITE) ?
	    STATE_WRITE : STATE_READ;

	this->state = state[args[0]];
	this->next = this->state != STATE_NONE ? this->state : this->prio;
}

vdev_queue_pending_add:entry
/(this->state == STATE_READ && this->prio == STATE_WRITE) ||
    (this->state == STATE_WRITE && this->prio == STATE_READ)/
{
	this->next = STATE_RW;
}

vdev_queue_pending_remove:entry
{
	this->prio = (args[1]->io_priority == ZIO_PRIORITY_SYNC_WRITE ||
	    args[1]->io_priority == ZIO_PRIORITY_ASYNC_WRITE) ?
	    STATE_WRITE : STATE_READ;

	this->reads = args[0]->vq_class[ZIO_PRIORITY_ASYNC_READ].vqc_active +
	    args[0]->vq_class[ZIO_PRIORITY_SYNC_READ].vqc_active -
	    (this->prio == STATE_READ ? 1 : 0);

	this->writes = args[0]->vq_class[ZIO_PRIORITY_ASYNC_WRITE].vqc_active +
	    args[0]->vq_class[ZIO_PRIORITY_SYNC_WRITE].vqc_active -
	    (this->prio == STATE_WRITE ? 1 : 0);

	this->state = state[args[0]];
	this->next = this->reads > 0 ?
	    (this->writes > 0 ? STATE_RW : STATE_READ) :
	    (this->writes > 0 ? STATE_WRITE : STATE_NONE);
}

vdev_queue_pending_add:entry,
vdev_queue_pending_remove:entry
/this->state != this->next/
{
	this->q = (vdev_queue_t *)arg0;
	printf("{ \"time\": \"%d\", \"entity\": \"%s\", \"state\": %d }\n",
	    timestamp - start,
	    basename(this->q->vq_vdev->vdev_path), this->next);

	state[this->q] = this->next;
}

tick-1sec
/timestamp - start > 300 * 1000000000/
{
	exit(0);
}


================================================
FILE: src/icons/LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: src/icons/README.md
================================================

# Statemap icons

The icons here are from the [svg-icon](https://leungwensen.github.io/svg-icon/)
project, and in particular the
["zero" family](https://leungwensen.github.io/svg-icon/#zero).



================================================
FILE: src/main.rs
================================================
/*
 * Copyright 2018 Joyent, Inc.
 */ 

/*
 * We don't want to get away with not using values that we must use.
 */
#![deny(unused_must_use)]

extern crate getopts;
use getopts::Options;
use getopts::HasArg;
use std::env;

#[macro_use]
extern crate serde_derive;

#[macro_use]
extern crate serde_json;

mod statemap;

use statemap::*;

macro_rules! fatal {
    ($fmt:expr) => ({
        eprint!(concat!("statemap: ", $fmt, "\n"));
        ::std::process::exit(1);
    });
    ($fmt:expr, $($arg:tt)*) => ({
        eprint!(concat!("statemap: ", $fmt, "\n"), $($arg)*);
        ::std::process::exit(1);
    });
}

fn usage(opts: Options) {
    println!("{}", opts.usage("Usage: statemap [options] FILE"));
    ::std::process::exit(0);
}

fn parse_offset(matches: &getopts::Matches, opt: &str) -> i64 {
    fn parse_offset_val(val: &str) -> Option<i64> {
        let mut mult: i64 = 1;
        let mut num = val;

        let suffixes: &[(&'static str, i64)] = &[
            ("ns", 1), ("us", 1_000), ("ms", 1_000_000),
            ("s", 1_000_000_000), ("sec", 1_000_000_000)
        ];

        for suffix in suffixes {
            if val.ends_with(suffix.0) {
                mult = suffix.1;
                num = &val[..val.len() - suffix.0.len()];
                break;
            }
        }

        /*
         * First attempt to parse our number as an integer, falling back
         * on parsing it as floating point if that fails (and being sure
         * to not allow some joker to specify "NaNms").
         */
        match num.parse::<i64>() {
            Err(_err) => {
                match num.parse::<f64>() {
                    Err(_err) => None,
                    Ok(val) => {
                        if val.is_nan() {
                            None
                        } else {
                            Some((val * mult as f64) as i64)
                        }
                    }
                }
            },
            Ok(val) => Some(val * mult)
        }
    }

    /*
     * We can safely unwrap here because we should only be here if the option
     * has been set.
     */
    let optval = matches.opt_str(opt).unwrap();

    match parse_offset_val(&optval) {
        Some(val) => val,
        None => fatal!(concat!("value for {} is not a valid ",
            "expression of time: \"{}\""), opt, optval)
    }
}

fn main() {
    struct Opt {
        name: (&'static str, &'static str),
        help: &'static str,
        hint: &'static str,
        hasarg: HasArg,
        alias: Option<&'static str>,
    }

    let opts: &[Opt] = &[
        Opt {
            name: ("b", "begin"),
            help: "time offset at which to begin statemap",
            hint: "TIME",
            hasarg: HasArg::Yes,
            alias: None,
        },
        Opt {
            name: ("e", "end"),
            help: "time offset at which to end statemap",
            hint: "TIME",
            hasarg: HasArg::Yes,
            alias: None,
        },
        Opt {
            name: ("d", "duration"),
            help: "time duration of statemap",
            hint: "TIME",
            hasarg: HasArg::Yes,
            alias: None,
        },
        Opt {
            name: ("c", "coalesce"),
            help: "coalesce target",
            hint: "TARGET",
            hasarg: HasArg::Yes,
            alias: None,
        },
        Opt {
            name: ("?", "help"),
            help: "print this usage message",
            hint: "",
            hasarg: HasArg::No,
            alias: None,
        },
        Opt {
            name: ("s", "sortby"),
            help: "state to sort by (defaults to entity name)",
            hint: "STATE",
            hasarg: HasArg::Yes,
            alias: None,
        },
        Opt {
            name: ("S", "stacksortby"),
            help: "state to sort stacked statemaps by",
            hint: "STATE",
            hasarg: HasArg::Yes,
            alias: None,
        },
        Opt {
            name: ("i", "ignore-tags"),
            help: "ignore tags in input",
            hint: "",
            hasarg: HasArg::No,
            alias: Some("ignoreTags"),
        },
        Opt {
            name: ("h", "state-height"),
            help: "height of each state, in pixels",
            hint: "PIXELS",
            hasarg: HasArg::Yes,
            alias: Some("stateHeight"),
        },
        Opt {
            name: ("n", "dry-run"),
            help: "ingest data, but do not generate output",
            hint: "",
            hasarg: HasArg::No,
            alias: None,
        },
    ];

    let mut args: Vec<String> = env::args().collect();

    /*
     * Iterate over our arguments and options, replacing any alias we find.
     * This allows us to (silently -- and inelegantly) remain backward
     * compatible with camel-cased options while moving to snake-cased ones.
     */
    for i in 0..args.len() {
        for opt in opts {
            if let Some(alias) = opt.alias {
                if args[i].find(alias) != None {
                    args[i] = args[i].replace(alias, opt.name.1);
                }
            }
        }
    }

    let mut parser = Options::new();

    /*
     * Load the parser with our options.
     */
    for opt in opts {
        parser.opt(opt.name.0, opt.name.1,
            opt.help, opt.hint, opt.hasarg, getopts::Occur::Optional);
    }

    let matches = match parser.parse(&args[1..]) {
        Ok(m) => { m }
        Err(f) => { fatal!("{}", f) }
    };

    if matches.opt_present("help") {
        usage(parser);
    }

    let mut begin: i64 = 0;
    let mut end: i64 = 0;

    let has_duration = matches.opt_present("duration");
    let has_begin = matches.opt_present("begin");
    let has_end = matches.opt_present("end");

    if has_duration {
        let duration = parse_offset(&matches, "duration");

        if has_begin {
            if has_end {
                fatal!("cannot specify all of begin, end, and duration");
            } else {
                begin = parse_offset(&matches, "begin");
                end = begin + duration;
            }
        } else {
            if has_end {
                end = parse_offset(&matches, "end");

                if duration > end {
                    fatal!("duration cannot exceed end offset");
                }

                begin = end - duration;
            } else {
                end = duration;
            }
        }
    } else {
        if has_end {
            end = parse_offset(&matches, "end")
        }

        if has_begin {
            begin = parse_offset(&matches, "begin");
            if end < begin {
                fatal!("begin offset must be less than end offset");
            }
        }
    }

    if matches.free.is_empty() {
        fatal!("must specify a data file");
    }

    let mut config = Config {
        begin: begin,
        end: end,
        notags: matches.opt_present("ignore-tags"),
        abstime: false,
        .. Default::default()
    };

    match matches.opt_str("coalesce") {
        Some(str) => match str.parse::<u64>() {
            Err(_err) => fatal!("coalesce factor must be an integer"),
            Ok(val) => config.maxrect = val
        }
        _ => {}
    }

    let mut svgconf: StatemapSVGConfig = Default::default();

    svgconf.sortby = matches.opt_str("sortby");
    svgconf.stacksortby = matches.opt_str("stacksortby");

    if let Some(str) = matches.opt_str("state-height") {
        match str.parse::<u32>() {
            Err(_err) => fatal!("state height must be an integer"),
            Ok(val) => svgconf.stripHeight = val
        }
    }

    let mut statemaps: Vec<Statemap> = vec![];

    for i in 0..matches.free.len() {
        let mut statemap = Statemap::new(&config);
        let filename = &matches.free[i];

        match statemap.ingest(filename) {
            Err(f) => { fatal!("could not ingest {}: {}", filename, f); }
            Ok(k) => { k }
        }

        if !config.abstime {
            /*
             * If our time configuration is not absolute, we just processed
             * our first statemap; change our time configuration to now be
             * absolute to key the time for every subsequent statemap based
             * on this first statemap.
             */
            assert!(i == 0);
            config.abstime = true;
            let timebounds = statemap.timebounds();
            config.begin = timebounds.0 as i64;
            config.end = timebounds.1 as i64;
        }

        statemaps.push(statemap);
    }

    if matches.opt_present("dry-run") {
        return;
    }

    let svg = StatemapSVG::new(&svgconf);

    match svg.output(&statemaps) {
        Err(f) => { fatal!("{}", f); }
        Ok(k) => { k }
    }
}


================================================
FILE: src/statemap-svg.css
================================================
/*
 * Copyright 2018, Joyent, Inc.
 */

.sansserif {
	font-family: Verdana, Arial, Helvetica, sans-serif;
}

.button {
        fill: #fff;
}

.statemap-border {
	fill: none;
	stroke: black;
	stroke-width: 0.5;
}

.statemap-highlight {
	fill:		blue;
}

.statemap-title {
	font-size:	10pt;
	font-weight:	bold;
	text-anchor:	middle;
	cursor:		default;
}

.statemap-timelabel {
	font-size:	8pt;
	text-anchor:	middle;
	cursor:		default;
}

.statemap-timebar {
	stroke:		blue;
	fill:		blue;
	stroke-width:	1px;
}

.statemap-timetext {
	font-size:	8pt;
	font-weight:	normal;
	fill:		blue;
	stroke:		blue;
	stroke-width:	0px;
	cursor:		default;
}

.statemap-timebreaktext {
	font-size:	8pt;
	font-weight:	bold;
	fill:		blue;
	stroke:		white;
	stroke-width:	0.3px;
	cursor:		default;
}

.statemap-subbar {
	stroke:		blue;
	fill:		blue;
	stroke-width:	1px;
	stroke-dasharray: 5.7;
}

.statemap-subbar-span {
	stroke:		blue;
	fill:		blue;
	stroke-width:	0.75px;
	stroke-dasharray: 1;
}

.statemap-subbar-text {
	font-size:	8pt;
	font-weight:	bold;
	fill:		blue;
	stroke:		white;
	stroke-width:	0.3px;
	cursor:		default;
}

.statemap-statebar {
	stroke:		blue;
	fill:		blue;
	stroke-width:	1px;
}

.statemap-statetext {
	font-size:	8pt;
	font-weight:	normal;
	fill:		blue;
	stroke:		blue;
	stroke-width:	0px;
	cursor:		default;
}

.statemap-timeline {
	stroke: black;
	stroke-width:	0.5pt;
	marker-start:	url(#startarrow);
	marker-end:	url(#endarrow);
}

.statemap-legend {
	stroke: black;
	stroke-width:	0.5pt;
}

.statemap-legend-highlighted {
	stroke:		blue;
	stroke-width:	3pt;
}

.statemap-legendlabel {
	font-size:	7pt;
	text-anchor:	middle;
	cursor:		default;
}

.statemap-tagbox {
	stroke:		black;
	stroke-width:	0.5pt;
	alignment-baseline:	hanging;
	text-anchor:	start;
	cursor:		default;
}

.statemap-tagbox-header {
	font-size:	9pt;
	font-weight:	bold;
	text-anchor:	start;
	font-style:	oblique;
	cursor:		default;
}

.statemap-tagbox-header-line {
	stroke:		black;
	stroke-width:	1pt;
}

.statemap-tagbox-tag {
	font-size:	9pt;
	cursor:		default;
}

.statemap-tagbox-tag-highlighted {
	font-weight:	bold;
}

.statemap-tagbox-select-header {
	font-size:	8pt;
	text-anchor:	start;
	font-style:	oblique;
	cursor:		default;
}

.statemap-tagbox-select {
	font-size:	8pt;
	text-anchor:	start;
	cursor:		default;
}

.statemap-tagbox-select-perc {
	font-size:	8pt;
	text-anchor:	end;
	cursor:		default;
}

.statemap-tagbox-select-header-line {
	stroke:		black;
	stroke-width:	0.5pt;
}

.statemap-tagbox-select-sum-line {
	stroke:		black;
	stroke-width:	0.25pt;
}

.statemap-tagbox-select-highlighted {
	font-weight:	bold;
}


================================================
FILE: src/statemap-svg.defs
================================================

<!-- This file is pulled into the <defs> section !-->

 <marker id="startarrow" markerWidth="10" markerHeight="7" 
    refX="10" refY="3.5" orient="auto">
      <polygon points="10 0, 10 7, 0 3.5" fill="black" />
    </marker>
    <marker id="endarrow" markerWidth="10" markerHeight="7" 
    refX="0" refY="3.5" orient="auto" markerUnits="strokeWidth">
        <polygon points="0 0, 10 3.5, 0 7" fill="black" />
    </marker>



================================================
FILE: src/statemap-svg.js
================================================
/*
 * Copyright 2018, Joyent, Inc.
 */

/*
 * This file is dropped into the generated SVG -- and if you're looking at
 * the generated SVG and wondering where this comes from, look for
 * statemap-svg.js in statemap's src directory.
 */

var g_transMatrix = [1, 0, 0, 1, 0, 0];		/* transform of statemap */
var g_svgDoc;					/* our SVG document */
var g_offset;					/* x offset of statemap */
var g_timelabel;				/* label for time spanned */
var g_timebar;					/* timebar, if any */
var g_statebar;					/* statebar, if any */
var g_height;					/* pixel height of statemap */
var g_width;					/* pixel width of statemap */
var g_statesel;					/* state selection, if any */
var g_tagsel;					/* tag selection, if any */
var g_tagvalsel;				/* tag val selection, if any */

var g_statemaps = [];				/* array of statemaps */

var timeunits = function (timeval)
{
	var i, rem;
	var suffixes = [ 'ns', 'μs', 'ms', 's' ];

	if (timeval === 0)
		return ('0');

	for (i = 0; (timeval > 1000 || timeval < -1000) &&
	    i < suffixes.length - 1; i++)
		timeval /= 1000;

	rem = Math.floor((timeval - Math.floor(timeval)) * 1000);

	return (Math.floor(timeval) + '.' +
	    (rem < 100 ? '0' : '') + (rem < 10 ? '0' : '') + rem +
	    suffixes[i]);
};

var timeFromMapX = function (mapX)
{
	var base, offs;
	var timeWidth = globals.timeWidth;

	/*
	 * Our base (in nanoseconds) is our X offset in the transformation
	 * matrix as a ratio of our total (scaled) width, times our timeWidth.
	 */
	base = (-g_transMatrix[4] / (g_transMatrix[0] * g_width)) * timeWidth;

	/*
	 * Our offset (in nanoseconds) is the X offset within the statemap
	 * as a ratio of the statemap width, times the number of nanoseconds
	 * visible in the statemap (which itself is the timeWidth divided by
	 * our scaling factor).
	 */
	offs = (mapX / g_width) * (timeWidth / g_transMatrix[0]);

	return (base + offs);
};

var timeToMapX = function (time)
{
	/*
	 * We take the ratio of the time of the timebar of the total time
	 * width times the width times the scale, and then add that to the
	 * X offset in the transformation matrix.
	 */
	return (((time / globals.timeWidth) * g_width *
	    g_transMatrix[0]) + g_transMatrix[4]);
};

var timeToText = function (time)
{
	var t;

	if (g_transMatrix[0] === 1 && globals.begin === 0) {
		t = 'offset = ' + timeunits(time);
	} else {
		t = 'offset = ' + timeunits(time) + ', ' +
		    timeunits(time + globals.begin) + ' overall';
	}

	if (globals.start) {
		var s = globals.start[0] +
		    (time + globals.start[1]) / 1000000000;

		t += ' (Epoch + ' + Math.floor(s) + 's)';
	}

	return (t);
};

var timeSetSpanLabel = function ()
{
	var t = 'span = ' + timeunits(globals.timeWidth / g_transMatrix[0]);

	if (g_transMatrix[0] != 1 || globals.begin !== 0)
		t += '; ' + timeToText(timeFromMapX(0));

	g_timelabel.textContent = t;
};

var initStatemap = function (statemap, elem, position)
{
	var i, highlight;
	var prefix = globals.entityPrefix + statemap + '-';

	g_statemaps[statemap].elem = elem;
	g_statemaps[statemap].position = position;
	g_statemaps[statemap].nentities = 0;

	/*
	 * Iterate over this statemap's children, looking for entities.
	 */
	for (i = 0; i < elem.childNodes.length; i++) {
		var id = elem.childNodes[i].id, entity;

		if (!id || id.indexOf(prefix) !== 0)
			continue;

		entity = {
			name: id.substr(prefix.length),
			element: elem.childNodes[i],
			position: position++,
			statemap: statemap
		};

		entity.description =
		    g_statemaps[statemap].entities[entity.name].description;

		g_entities[id] = entity;
		g_statemaps[statemap].nentities++;
	}

	/*
	 * Determine the legend that this statemap is using.
	 */
	for (i = statemap; i >= 0; i--) {
		if (g_svgDoc.getElementById('statemap-legend-' + i + '-0')) {
			g_statemaps[statemap].legend = i;
			break;
		}
	}

	console.assert(i >= 0);

	/*
	 * Dynamically change the styling of the highlight rectangle.
	 */
	highlight = g_svgDoc.getElementById('statemap-' +
	    statemap + '-highlight');
	highlight.classList.add('statemap-highlight');

	return (position);
};

var init = function (evt)
{
	var i = 0, position = 0, statemap;

	g_svgDoc = evt.target.ownerDocument;
	g_entities = [];

	while ((statemap = g_svgDoc.getElementById('statemap-' + i)) != null)
		position = initStatemap(i++, statemap, position);

	g_height = globals.pixelHeight;
	g_width = globals.pixelWidth;

	g_offset = evt.target.getAttributeNS(null, 'width') -
	    (g_width + globals.tagWidth);

	g_timelabel = g_svgDoc.getElementById('statemap-timelabel');
	timeSetSpanLabel();

	g_timebar = undefined;
};

var entityForEachDatum = function (entity, time, etime, func)
{
	var data = g_statemaps[entity.statemap].data[entity.name];

	var idx, length = data.length;
	var floor = 0;
	var ceil = length;
	var datum, t, span;

	if (length === 0 || (data[0].t > time && !etime))
		return;

	if (data[0].t > time) {
		idx = 0;
	} else {
		/*
		 * Binary search our data until we find a datum that contains
		 * the start of our time range.
		 */
		for (;;) {
			idx = floor + Math.floor((ceil - floor) / 2);

			if (data[idx].t > time) {
				ceil = idx;
				continue;
			}

			if (idx + 1 == length || data[idx + 1].t > time)
				break;

			floor = idx;
		}
	}

	/*
	 * If we don't have a specified etime, we have found the datum that
	 * contains the time; just call our function and return.
	 */
	if (!etime) {
		func(data[idx], idx, 1);
		return;
	}

	/*
	 * Now we're going to iterate forward, calling our function until we
	 * get past our specified etime.
	 */
	for (; idx < length; idx++) {
		datum = data[idx];

		if (datum.t > etime)
			return;

		if ((t = datum.t) < time)
			t = time;

		if (idx + 1 == length || data[idx + 1].t > etime) {
			/*
			 * This datum contains the end of the range; our span
			 * is our etime minus this datum's start time (or our
			 * specified time, whichever is greater).
			 */
			span = etime - t;
		} else {
			/*
			 * The end of the datum is covered by the range; our
			 * span is the time width of the datum.
			 */
			span = data[idx + 1].t - t;
		}

		func(datum, idx, span);
	}
};

var entityDatum = function (entity, idx)
{
	var data = g_statemaps[entity.statemap].data[entity.name];
	var datum = data[idx];
	var rval = { time: datum.t };

	if (datum.s instanceof Object) {
		rval.states = datum.s;
	} else {
		rval.state = datum.s;
	}

	if (idx + 1 < data.length) {
		rval.etime = data[idx + 1].t;
	} else {
		rval.etime = globals.timeWidth + globals.begin;
	}

	return (rval);
};

var entityBreakdown = function (entity, time, etime)
{
	var data = g_statemaps[entity.statemap].data[entity.name];
	var rval = {};

	var idx, length = data.length;
	var floor = 0;
	var ceil = length;
	var datum, t, span, state;

	time += g_statemaps[entity.statemap].offset;

	if (length === 0 || data[0].t > time)
		return ({});

	/*
	 * Binary search our data until we find a datum that contains the
	 * specified time.
	 */
	for (;;) {
		idx = floor + Math.floor((ceil - floor) / 2);

		if (data[idx].t > time) {
			ceil = idx;
			continue;
		}

		if (idx + 1 == length || data[idx + 1].t > time)
			break;

		floor = idx;
	}

	/*
	 * If we don't have a specified etime, we want to just return the state
	 * breakdown at the specified time.
	 */
	if (!etime) {
		datum = data[idx];

		if (datum.s instanceof Object)
			return (datum.s);

		rval[datum.s] = 1.0;

		return (rval);
	}

	/*
	 * Now we're going to iterate forward until we get past our specified
	 * etime.
	 */
	for (; idx < length; idx++) {
		datum = data[idx];

		if (datum.t > etime)
			break;

		if ((t = datum.t) < time)
			t = time;

		if (idx + 1 == length || data[idx + 1].t > etime) {
			/*
			 * This datum contains the end of the range; our span
			 * is our etime minus this datum's start time (or our
			 * specified time, whichever is greater).
			 */
			span = etime - t;
		} else {
			/*
			 * The end of the datum is covered by the range; our
			 * span is the time width of the datum.
			 */
			span = data[idx + 1].t - t;
		}

		/*
		 * Express our span as a ratio of the overall time.
		 */
		span /= (etime - time);

		if (datum.s instanceof Object) {
			for (state in datum.s) {
				if (!rval.hasOwnProperty(state))
					rval[state] = 0;

				rval[state] += (datum[state] * span);
			}
		} else {
			state = datum.s;

			if (!rval.hasOwnProperty(state))
				rval[state] = 0;

			rval[state] += span;
		}
	}

	return (rval);
};

var statebarCreateBar = function (statebar, x1, y1, x2, y2)
{
	var parent = statebar.parent;

	var bar = g_svgDoc.createElementNS(parent.namespaceURI, 'line');
	bar.classList.add('statemap-statebar');
	bar.x1.baseVal.value = x1;
	bar.y1.baseVal.value = y1;
	bar.x2.baseVal.value = x2;
	bar.y2.baseVal.value = y2;
	parent.appendChild(bar);
	statebar.bars.push(bar);
};

var statebarCreate = function (elem, idx)
{
	var parent = g_statemaps[0].elem.parentNode.parentNode;
	var statebar = { parent: parent, hidden: false };
	var entity = g_entities[elem.parentNode.id];
	var statemap = g_statemaps[entity.statemap];
	var states = statemap.states;
	var datum = entityDatum(entity, idx);
	var pos = (entity.position * globals.stripHeight) +
	    (entity.statemap * globals.smargin);
	var x = globals.lmargin - 2;
	var y = globals.tmargin + pos;
	var elbow = { x: 8, y: 10 };
	var nudge = { x: 3, y: 2 };
	var direction = 1, anchor;
	var anchors = [ 'start', 'end' ];
	var text;

	if (pos < (globals.totalHeight - globals.tmargin) / 2) {
		direction = 1;
		anchor = 1;
	} else {
		direction = -1;
		anchor = 0;
	}

	statebar.bars = [];

	/*
	 * We have three bars to draw:  our bar that runs the height of the
	 * strip, followed by our elbow.
	 */
	statebarCreateBar(statebar, x, y, x, y + globals.stripHeight);

	y += 0.5 * globals.stripHeight;
	statebarCreateBar(statebar, x - elbow.x, y, x, y);

	x -= elbow.x;
	statebarCreateBar(statebar, x, y, x, y + (elbow.y * direction));

	/*
	 * Now create the text at the end of the elbow.
	 */
	y += (elbow.y + nudge.y) * direction;
	x += nudge.x;
	text = g_svgDoc.createElementNS(parent.namespaceURI, 'text');
	text.classList.add('sansserif');
	text.classList.add('statemap-statetext');

	var t = statemap.entityKind + ' ' + entity.name;

	if (entity.description)
		t += ' (' + entity.description + ')';

	if (datum.hasOwnProperty('state')) {
		t += ', ' + states[datum.state].name;
	} else {
		var i, total = 0, max = 0, maxstate;

		for (i in datum.states) {
			total += datum.states[i];

			if (datum.states[i] > max) {
				maxstate = i;
				max = datum.states[i];
			}
		}

		t += ', ' + Math.floor((datum.states[maxstate] / total) * 100);
		t += '% ' + states[maxstate].name;
	}

	t += ' at ' + timeunits(datum.time);
	t += ' for ' + timeunits(datum.etime - datum.time);

	text.appendChild(g_svgDoc.createTextNode(t));
	text.setAttributeNS(null, 'x', x);
	text.setAttributeNS(null, 'y', y);
	text.setAttributeNS(null, 'transform',
	    'rotate(270,' + x + ',' + y + ')');
	text.setAttributeNS(null, 'text-anchor', anchors[anchor]);
	text.addEventListener('click', function () {
		statebarRemove(statebar);
		stateselUpdate();
	});

	parent.appendChild(text);
	statebar.bars.push(text);

	statebar.entity = entity;

	if (g_statemaps.length == 1)
		return (statebar);

	/*
	 * If we have more than one statemap, we want to add a bar to the right
	 * side to indicate which statemap this is.
	 */
	var pos = (statemap.position * globals.stripHeight) +
	    (entity.statemap * globals.smargin);

	x = globals.lmargin + g_width + 2;
	y = globals.tmargin + pos;

	var pos = (statemap.position * globals.stripHeight) +
	    (entity.statemap * globals.smargin);

	var height = statemap.nentities * globals.stripHeight;

	statebarCreateBar(statebar, x, y, x, y + height);

	y += 0.5 * height;
	statebarCreateBar(statebar, x + elbow.x, y, x, y);

	x += elbow.x;
	statebarCreateBar(statebar, x, y, x, y + (elbow.y * direction));

	/*
	 * Now create the text at the end of the elbow.
	 */
	y += (elbow.y + nudge.y) * direction;
	x -= nudge.x;
	text = g_svgDoc.createElementNS(parent.namespaceURI, 'text');
	text.classList.add('sansserif');
	text.classList.add('statemap-statetext');
	text.appendChild(g_svgDoc.createTextNode(statemap.title));
	text.setAttributeNS(null, 'x', x);
	text.setAttributeNS(null, 'y', y);
	text.setAttributeNS(null, 'transform',
	    'rotate(90,' + x + ',' + y + ')');
	text.setAttributeNS(null, 'text-anchor', anchors[anchor ^ 1]);

	parent.appendChild(text);
	statebar.bars.push(text);

	return (statebar);
};

var statebarRemove = function (statebar)
{
	var i;

	if (!statebar)
		return;

	if (statebar.bars) {
		for (i = 0; i < statebar.bars.length; i++)
			statebar.parent.removeChild(statebar.bars[i]);
	}

	statebar.bars = undefined;
	statebar.entity = undefined;
};

var timebarRemove = function (timebar)
{
	if (!timebar)
		return;

	timebarRemoveSubbar(timebar);

	if (timebar.bar && !timebar.hidden) {
		timebar.parent.removeChild(timebar.bar);
		timebar.parent.removeChild(timebar.text);
	}

	if (timebar.breakdown) {
		var i;

		for (i = 0; i < timebar.breakdown.length; i++) {
			var elem = timebar.breakdown[i];
			elem.parentNode.removeChild(elem);
		}
	}

	timebar.bar = undefined;
	timebar.text = undefined;
	timebar.breakdown = undefined;
};

var timebarSetBarLocation = function (bar, mapX)
{
	var absX = mapX + g_offset;
	var nubheight = 15;

	bar.x1.baseVal.value = absX;
	bar.y1.baseVal.value = globals.tmargin - nubheight;
	bar.x2.baseVal.value = absX;
	bar.y2.baseVal.value = globals.tmargin + g_height;
};

var timebarSetSubbarLocation = function (subbar, mapX, timebarX)
{
	var absX = mapX + g_offset, x;
	var bar = subbar.bar;
	var span = subbar.span;
	var text = subbar.text;
	var nudge = { x: 0, y: 10 };

	bar.x1.baseVal.value = absX;
	bar.y1.baseVal.value = globals.tmargin;
	bar.x2.baseVal.value = absX;
	bar.y2.baseVal.value = globals.tmargin + g_height;

	span.x1.baseVal.value = timebarX + g_offset;
	span.y1.baseVal.value = subbar.y;
	span.x2.baseVal.value = absX;
	span.y2.baseVal.value = subbar.y;

	x = (timebarX < mapX ? timebarX : mapX) +
	    Math.abs(timebarX - mapX) / 2;

	text.setAttributeNS(null, 'text-anchor', 'middle');
	text.setAttributeNS(null, 'x', x + g_offset);
	text.setAttributeNS(null, 'y', subbar.y + nudge.y);
};

var timebarSetTextLocation = function (text, mapX)
{
	var absX = mapX + g_offset;
	var nudge = { x: 3, y: 5 };
	var direction, anchor;
	var time;

	/*
	 * The side of the timebar that we actually render the text containing
	 * the offset and the time depends on the location of our timebar with
	 * respect to the center of the visible statemap.
	 */
	if (mapX < (g_width / 2)) {
		direction = 1;
		anchor = 'start';
	} else {
		direction = -1;
		anchor = 'end';
	}

	text.setAttributeNS(null, 'x', absX + (direction * nudge.x));
	text.setAttributeNS(null, 'y', globals.tmargin - nudge.y);
	text.setAttributeNS(null, 'text-anchor', anchor);

	time = timeFromMapX(mapX);
	text.childNodes[0].textContent = timeToText(time);

	return (time);
};

var timebarHideSubbar = function (timebar)
{
	var parent, subbar;

	if (!timebar || !(subbar = timebar.subbar) || subbar.hidden)
		return;

	parent = timebar.parent;
	parent.removeChild(subbar.bar);
	parent.removeChild(subbar.span);
	parent.removeChild(subbar.text);

	subbar.hidden = true;
};

var timebarShowSubbar = function (timebar)
{
	var parent, subbar, mapX;

	if (!timebar || !(subbar = timebar.subbar) || !subbar.hidden)
		return;

	mapX = timeToMapX(subbar.time)

	if (mapX < 0 || mapX >= g_width)
		return;

	timebarSetSubbarLocation(subbar, mapX, timebar.x);

	parent = timebar.parent;
	parent.appendChild(subbar.bar);
	parent.appendChild(subbar.span);
	parent.appendChild(subbar.text);

	subbar.hidden = false;
}

var timebarHide = function (timebar)
{
	if (!timebar || timebar.hidden || !timebar.bar)
		return;

	timebar.parent.removeChild(timebar.bar);
	timebar.parent.removeChild(timebar.text);
	timebar.hidden = true;
	timebarHideSubbar(timebar);
};

var timebarShow = function (timebar)
{
	var mapX;

	if (!timebar || !timebar.hidden)
		return;

	mapX = timeToMapX(timebar.time);

	if (mapX < 0 || mapX >= g_width)
		return;

	timebarSetBarLocation(timebar.bar, mapX);
	timebarSetTextLocation(timebar.text, mapX);
	timebar.x = mapX;

	timebar.parent.appendChild(timebar.bar);
	timebar.parent.appendChild(timebar.text);
	timebar.hidden = false;

	timebarShowSubbar(timebar);
};

var timebarSetMiddle = function (timebar)
{
	var mapX = g_width / 2;

	if (!timebar || !timebar.bar)
		return;

	/*
	 * This is just an algebraic rearrangement of the mapX calculation
	 * in timebarShow(), above.
	 */
	g_transMatrix[4] = -(((timebar.time / globals.timeWidth) * g_width *
	    g_transMatrix[0]) - mapX);
};

var timebarSetBreakdown = function (time)
{
	var breakdown, state, total = [];
	var entity;
	var sum = {};
	var rval = [];

	var click = function (statemap, s) {
		return (function (evt) { legendclick(evt, statemap, s); });
	};

	time += globals.begin;

	for (entity in g_entities) {
		var statemap = g_statemaps[g_entities[entity].statemap].legend;

		breakdown = entityBreakdown(g_entities[entity], time);

		if (!total[statemap]) {
			total[statemap] = {};
			sum[statemap] = 0;
		}

		for (state in breakdown) {
			if (!total[statemap].hasOwnProperty(state))
				total[statemap][state] = 0;

			sum[statemap] += breakdown[state];
			total[statemap][state] += breakdown[state];
		}
	}

	var settotal = function (statemap, state) {
		var legend, parent, text;
		var x, y, width, height, t;
		var nudge = 3;

		/*
		 * Iterate down until we find a valid legend.  We know that
		 * that there will be at least one, but we break out of the
		 * loop anyway if we don't find it to allow the failure mode
		 * here to be an unreferenced property rather than an
		 * inifinite loop.
		 */
		legend = g_svgDoc.getElementById('statemap-legend-' +
		    statemap + '-' + state);

		parent = legend.parentNode;

		x = parseInt(legend.getAttributeNS(null, 'x'), 10);
		y = parseInt(legend.getAttributeNS(null, 'y'), 10);
		width = parseInt(legend.getAttributeNS(null, 'width'), 10);
		height = parseInt(legend.getAttributeNS(null, 'height'), 10);

		t = Math.floor(total[statemap][state]) + ' (' +
		    Math.floor((total[statemap][state] /
		    sum[statemap]) * 100) + '%)';

		text = g_svgDoc.createElementNS(parent.namespaceURI, 'text');
		text.classList.add('sansserif');
		text.classList.add('statemap-timebreaktext');

		text.appendChild(g_svgDoc.createTextNode(t));
		text.setAttributeNS(null, 'x', x + (width / 2));
		text.setAttributeNS(null, 'y', y + (height / 2) + nudge);
		text.setAttributeNS(null, 'text-anchor', 'middle');

		text.addEventListener('click',
		    click(statemap, g_statemaps[statemap].states[state].value));

		parent.appendChild(text);
		rval.push(text);
	};

	for (statemap in total) {
		for (state in total[statemap])
			settotal(statemap, state);
	}

	return (rval);
};

var timebarCreate = function (mapX)
{
	var parent = g_statemaps[0].elem.parentNode.parentNode;
	var bar, text;
	var timebar = { parent: parent, hidden: false };

	bar = g_svgDoc.createElementNS(parent.namespaceURI, 'line');
	bar.classList.add('statemap-timebar');

	timebarSetBarLocation(bar, mapX);
	parent.appendChild(bar);

	text = g_svgDoc.createElementNS(parent.namespaceURI, 'text');
	text.classList.add('sansserif');
	text.classList.add('statemap-timetext');
	text.appendChild(g_svgDoc.createTextNode(''));

	timebar.time = timebarSetTextLocation(text, mapX);
	timebar.breakdown = timebarSetBreakdown(timebar.time);
	timebar.x = mapX;

	text.addEventListener('click', function () {
		timebarRemove(timebar);
		stateselUpdate();
	});

	parent.appendChild(text);

	timebar.bar = bar;
	timebar.text = text;

	return (timebar);
};

var timebarCreateSubbar = function (timebar, mapX, absY)
{
	var parent = timebar.parent;
	var subbar, bar, span, text, time, delta;

	bar = g_svgDoc.createElementNS(parent.namespaceURI, 'line');
	bar.classList.add('statemap-subbar');

	span = g_svgDoc.createElementNS(parent.namespaceURI, 'line');
	span.classList.add('statemap-subbar-span');

	time = timeFromMapX(mapX);
	delta = Math.abs(timebar.time - time);

	text = g_svgDoc.createElementNS(parent.namespaceURI, 'text');
	text.classList.add('sansserif');
	text.classList.add('statemap-subbar-text');
	text.appendChild(g_svgDoc.createTextNode(timeunits(delta)));

	var subbar = { bar: bar, span: span, text: text,
	    time: time, y: absY, hidden: false };

	timebarSetSubbarLocation(subbar, mapX, timebar.x);

	parent.appendChild(bar);
	parent.appendChild(span);
	parent.appendChild(text);

	timebar.subbar = subbar;
}

var timebarRemoveSubbar = function (timebar)
{
	var subbar = timebar.subbar;

	if (!subbar)
		return;

	if (!subbar.hidden) {
		timebar.parent.removeChild(subbar.bar);
		timebar.parent.removeChild(subbar.span);
		timebar.parent.removeChild(subbar.text);
	}

	timebar.subbar = undefined;
}

var stateselTagvalSelect = function (evt, tagval)
{
	var tagdefs = {};
	var i, entity;
	var state, tags;
	var child;
	var highlight = 'statemap-tagbox-select-highlighted';

	if (g_statesel == undefined)
		return;

	state = g_statesel.state;
	tags = g_statemaps[g_statesel.statemap].tags;

	if (g_tagvalsel && g_tagvalsel.selected) {
		for (i = 0; i < g_tagvalsel.selected.length; i++) {
			child = g_tagvalsel.selected[i];
			child.removeAttribute('fill-opacity');
		}

		if (g_tagvalsel.element)
			g_tagvalsel.element.classList.remove(highlight);

		/*
		 * If our selection matches the selection that we have already
		 * made, then we are unselecting this tag value; we need only
		 * return.
		 */
		if (g_tagvalsel.tag == g_tagsel.tag &&
		    g_tagvalsel.tagval == tagval) {
			g_tagvalsel = undefined;
			return;
		}
	}

	g_tagvalsel = { selected: [], tag: g_tagsel.tag, tagval: tagval };

	evt.target.classList.add(highlight);
	g_tagvalsel.element = evt.target;

	/*
	 * Iterate over all of our tag definitions, looking for a match where
	 * the specified tag (for the specified state) matches the specified
	 * tag value.
	 */
	for (i = 0; i < tags.length; i++) {
		if (tags[i].state != state)
			continue;

		if (tags[i][g_tagsel.tag] != tagval)
			continue;

		tagdefs[i] = true;
	}

	/*
	 * Now for each entity, we will plow through every rectangle.
	 */
	for (id in g_entities) {
		var entity = g_entities[id];
		var elem = entity.element;
		var data = g_statemaps[entity.statemap].data[entity.name];
		var j = 0;

		for (i = 0; i < elem.childNodes.length; i++) {
			child = elem.childNodes[i];

			if (child.nodeName != 'rect')
				continue;

			var datum = data[j++];
			var tag;

			if (datum.s instanceof Object) {
				if (!datum.s[state])
					continue;
			} else {
				if (datum.s != state)
					continue;
			}

			if (!datum.g)
				continue;

			var ratio = 0;

			for (tag in datum.g) {
				if (tagdefs[tag])
					ratio += datum.g[tag];
			}

			if (ratio === 0)
				continue;

			/*
			 * At this point we have found a rectangle that we
			 * want to color, and we know the degree that we
			 * want to color it!
			 */
			child.setAttributeNS(null, 'fill-opacity', 1 - ratio);
			g_tagvalsel.selected.push(child);
		}
	}
};

var stateselUpdate = function ()
{
	var base, etime, nentities = 0;
	var state, entity;
	var bytag = {}, tagval;
	var header, i, tags;

	if (g_statesel == undefined)
		return;

	state = g_statesel.state;
	tags = g_statemaps[g_statesel.statemap].tags;

	var sum = function (datum, id, span) {
		var tid, tag;

		if (!(datum.s instanceof Object)) {
			if (datum.s != state)
				return;
		} else {
			var ratio;

			if (!(ratio = datum.s[state]))
				return;
		}

		if (!datum.g)
			return;

		for (tid in datum.g) {
			tag = tags[tid];

			if (tag.state != state)
				continue;

			if (!(tagval = tag[g_tagsel.tag]))
				continue;

			if (!bytag[tagval])
				bytag[tagval] = 0;

			bytag[tagval] += span * datum.g[tid];
		}
	};

	if (!g_tagsel)
		return;

	header = '';

	if (g_statebar && g_statebar.entity) {
		header = g_statemaps[g_statesel.statemap].entityKind + ' ' +
		    g_statebar.entity.name + ' ';
	}

	header += 'by ' + g_tagsel.tag + ' ';

	if (g_timebar && g_timebar.bar) {
		base = g_timebar.time + globals.begin;
		etime = 0;
		header += 'at ' + timeunits(g_timebar.time);
	} else {
		base = timeFromMapX(0) + globals.begin;
		etime = timeFromMapX(g_width) + globals.begin;
		header += 'over span';
	}

	header = header.charAt(0).toUpperCase() + header.substr(1) + ':';

	if (g_statebar && g_statebar.entity) {
		entityForEachDatum(g_statebar.entity, base, etime, sum);
		nentities++;
	} else {
		/*
		 * For each entity, we need to determine the amount of time
		 * in our selected state.
		 */
		for (entity in g_entities) {
			entityForEachDatum(g_entities[entity],
			    base, etime, sum);
			nentities++;
		}
	}

	var sorted = Object.keys(bytag).sort(function (lhs, rhs) {
		if (bytag[lhs] < bytag[rhs]) {
			return (1);
		} else if (bytag[lhs] > bytag[rhs]) {
			return (-1);
		} else {
			return (0);
		}
	});

	var divisor;

	if (etime === 0) {
		divisor = nentities;
	} else {
		divisor = (etime - base) * nentities;
	}

	var x = g_statesel.x;
	var y = g_statesel.y + 10;

	var elem = g_svgDoc.getElementById('statemap-tagbox-select');

	while (elem.childNodes.length > 0)
		elem.removeChild(elem.childNodes[0]);

	if (g_tagvalsel && g_tagvalsel.element)
		g_tagvalsel.element = undefined;

	var text = g_svgDoc.createElementNS(elem.namespaceURI, 'text');
	text.classList.add('statemap-tagbox-select-header');
	text.classList.add('sansserif');

	text.appendChild(g_svgDoc.createTextNode(header));
	text.setAttributeNS(null, 'x', x);
	text.setAttributeNS(null, 'y', y);
	elem.appendChild(text);
	y += 9;

	var line = g_svgDoc.createElementNS(elem.namespaceURI, 'line');
	line.classList.add('statemap-tagbox-select-header-line');
	line.x1.baseVal.value = x - 2;
	line.y1.baseVal.value = y;
	line.x2.baseVal.value = g_statesel.x2;
	line.y2.baseVal.value = y;
	elem.appendChild(line);
	y += 18;

	var bmargin = 60;
	var ttl = 0;
	var ellipsis = false;

	var click = function (tv) {
		return (function (evt) { stateselTagvalSelect(evt, tv); });
	};

	for (i = 0; i <= sorted.length; i++) {
		var t, perc;

		if (i < sorted.length) {
			perc = (bytag[sorted[i]] / divisor) * 100.0;
			tagval = sorted[i];
			ttl += perc;

			if (y > globals.totalHeight - bmargin) {
				if (ellipsis)
					continue;

				ellipsis = true;
				tagval = '...';
			}
		} else {
			perc = ttl;
			tagval = 'total';

			y -= 5;
			line = g_svgDoc.createElementNS(elem.namespaceURI,
			    'line');
			line.classList.add('statemap-tagbox-select-sum-line');
			line.x1.baseVal.value = x - 2;
			line.y1.baseVal.value = y;
			line.x2.baseVal.value = g_statesel.x2;
			line.y2.baseVal.value = y;
			elem.appendChild(line);
			y += 15;
		}

		if (i != sorted.length && ellipsis) {
			t = '...';
		} else {
			t = Math.trunc(perc) + '.' +
			    (Math.round(perc * 100) % 100) + '%';
		}

		text = g_svgDoc.createElementNS(elem.namespaceURI, 'text');
		text.classList.add('statemap-tagbox-select-perc');
		text.classList.add('sansserif');
		text.appendChild(g_svgDoc.createTextNode(t));
		text.setAttributeNS(null, 'x', x + 45);
		text.setAttributeNS(null, 'y', y);
		elem.appendChild(text);

		text = g_svgDoc.createElementNS(elem.namespaceURI, 'text');
		text.classList.add('statemap-tagbox-select');
		text.classList.add('sansserif');
		text.appendChild(g_svgDoc.createTextNode(tagval));
		text.setAttributeNS(null, 'x', x + 50);
		text.setAttributeNS(null, 'y', y);

		/*
		 * If we already have a tag value selection and it matches
		 * what we're about to display, indicate as much by
		 * highlighting it.
		 */
		if (g_tagvalsel && g_tagvalsel.tag == g_tagsel.tag &&
		    g_tagvalsel.tagval == tagval) {
			var highlight = 'statemap-tagbox-select-highlighted';
			text.classList.add(highlight);
			g_tagvalsel.element = text;
		}

		text.addEventListener('click', click(tagval));

		elem.title = t;
		elem.appendChild(text);
		y += 15;
	}
};

var stateselTagSelect = function (evt, tag)
{
	var elem, prefix = 'statemap-tagbox-tag-';

	if (g_tagsel) {
		elem = g_svgDoc.getElementById(prefix + g_tagsel.tag);
		elem.classList.remove(prefix + 'highlighted');

		if (g_tagsel.tag == tag) {
			g_tagsel = undefined;
			stateselUpdate();
			return;
		}
	}

	elem = g_svgDoc.getElementById(prefix + tag);
	elem.classList.add(prefix + 'highlighted');
	g_tagsel = { tag: tag };
	stateselUpdate();
};

var stateselClearTagbox = function ()
{
	var tagbox = g_svgDoc.getElementById('statemap-tagbox'), elem;

	if (!tagbox)
		return;

	while (tagbox.childNodes.length > 0)
		tagbox.removeChild(tagbox.childNodes[0]);

	elem = g_svgDoc.getElementById('statemap-tagbox-select');

	while (elem.childNodes.length > 0)
		elem.removeChild(elem.childNodes[0]);
};

var stateselSelect = function (statemap, state)
{
	var legend = g_svgDoc.getElementById('statemap-legend-' +
	    g_statemaps[statemap].legend + '-' + state);
	var states = g_statemaps[statemap].states;
	var alltags = g_statemaps[statemap].tags;
	var tags = {};
	var i, t;
	var lmargin = 20;
	var offset = globals.lmargin + globals.pixelWidth;

	legend.classList.add('statemap-legend-highlighted');
	stateselClearTagbox();

	t = 'tags for ' + states[state].name;

	var tagbox = g_svgDoc.getElementById('statemap-tagbox');
	var x = offset + lmargin;
	var y = globals.tmargin;
	var x2 = x + (globals.tagWidth - lmargin);

	var text = g_svgDoc.createElementNS(tagbox.namespaceURI, 'text');
	text.classList.add('statemap-tagbox-header');
	text.classList.add('sansserif');

	text.appendChild(g_svgDoc.createTextNode(t));
	text.setAttributeNS(null, 'x', x);
	text.setAttributeNS(null, 'y', y);
	tagbox.appendChild(text);
	y += 10;

	var line = g_svgDoc.createElementNS(tagbox.namespaceURI, 'line');
	line.classList.add('statemap-tagbox-header-line');
	line.x1.baseVal.value = x - 2;
	line.y1.baseVal.value = y;
	line.x2.baseVal.value = x2;
	line.y2.baseVal.value = y;
	tagbox.appendChild(line);
	y += 20;

	/*
	 * Now add text for each possible tag for this state.
	 */
	for (i = 0; i < alltags.length; i++) {
		if (alltags[i].state !== state)
			continue;

		for (t in alltags[i]) {
			if (t == 'state' || t == 'tag')
				continue;

			tags[t] = true;
		}
	}

	tags = Object.keys(tags).sort();

	var click = function (tag) {
		return (function (evt) { stateselTagSelect(evt, tag); });
	};

	for (i = 0; i < tags.length; i++) {
		text = g_svgDoc.createElementNS(tagbox.namespaceURI, 'text');
		text.classList.add('statemap-tagbox-tag');
		text.classList.add('sansserif');
		text.id = 'statemap-tagbox-tag-' + tags[i];

		text.appendChild(g_svgDoc.createTextNode(tags[i]));
		text.setAttributeNS(null, 'x', x);
		text.setAttributeNS(null, 'y', y);
		text.addEventListener('click', click(tags[i]));

		tagbox.appendChild(text);
		y += 18;
	}

	g_statesel = { statemap: statemap, state: state, x: x, y: y, x2: x2 };
	stateselUpdate();
};

var stateselClear = function ()
{
	var state, statemap, legend;

	if (g_statesel == undefined)
		return (-1);

	state = g_statesel.state;
	statemap = g_statesel.statemap;
	legend = g_svgDoc.getElementById('statemap-legend-' +
	    statemap + '-' + state);
	legend.classList.remove('statemap-legend-highlighted');

	stateselClearTagbox();
	g_statesel = undefined;
	g_tagsel = undefined;

	return (state);
};

var statemapsUpdate = function ()
{
	var i;
	var newMatrix = 'matrix(' +  g_transMatrix.join(' ') + ')';

	for (i = 0; i < g_statemaps.length; i++) {
		g_statemaps[i].elem.setAttributeNS(null,
		    'transform', newMatrix);
	}
};

/*
 * All of the following *click() functions are added at the time of statemap
 * generation.
 */
var legendclick = function (evt, statemap, state)
{
	if (globals.notags || stateselClear() == state)
		return;

	stateselSelect(statemap, state);
	stateselUpdate();
};

var mapclick = function (evt, idx)
{
	var x = evt.clientX - g_offset;

	if (evt.shiftKey || evt.altKey) {
		if (!g_timebar || !g_timebar.bar)
			return;

		timebarRemoveSubbar(g_timebar);
		timebarCreateSubbar(g_timebar, x, evt.clientY);
		return;
	}

	timebarRemove(g_timebar);
	g_timebar = timebarCreate(x);

	statebarRemove(g_statebar);
	g_statebar = statebarCreate(evt.target, idx);

	stateselUpdate();
};

var panclick = function (dx, dy)
{
	var minX = -(g_width * g_transMatrix[0] - g_width);
	var minY = -(g_height * g_transMatrix[0] - g_height);

	g_transMatrix[4] += dx;
	g_transMatrix[5] += dy;

	timebarHide(g_timebar);

	if (g_transMatrix[4] > 0)
		g_transMatrix[4] = 0;

	if (g_transMatrix[4] < minX)
		g_transMatrix[4] = minX;

	if (g_transMatrix[5] > 0)
		g_transMatrix[5] = 0;

	if (g_transMatrix[5] < minY)
		g_transMatrix[5] = minY;

	timeSetSpanLabel();
	statemapsUpdate();
	timebarShow(g_timebar);
	stateselUpdate();
};

var zoomclick = function (scale)
{
	var i;

	timebarHide(g_timebar);

	for (i = 0; i < g_transMatrix.length; i++) {
		/*
		 * We don't scale the Y direction on a zoom.
		 */
		if (i != 3)
			g_transMatrix[i] *= scale;
	}

	var minX = -(g_width * g_transMatrix[0] - g_width);
	var minY = -(g_height * g_transMatrix[0] - g_height);

	g_transMatrix[4] += (1 - scale) * g_width / 2;
	timebarSetMiddle(g_timebar);

	if (g_transMatrix[4] > 0)
		g_transMatrix[4] = 0;

	if (g_transMatrix[4] < minX)
		g_transMatrix[4] = minX;

	if (g_transMatrix[5] > 0)
		g_transMatrix[5] = 0;

	if (g_transMatrix[5] < minY)
		g_transMatrix[5] = minY;

	if (g_transMatrix[0] < 1)
		g_transMatrix = [1, 0, 0, 1, 0, 0];

	timeSetSpanLabel();
	statemapsUpdate();
	timebarShow(g_timebar);
	stateselUpdate();
};


================================================
FILE: src/statemap.rs
================================================
/*
 * Copyright 2020 Joyent, Inc. and other contributors
 */ 

extern crate memmap;
extern crate serde;
extern crate serde_json;
extern crate natord;
extern crate palette;
extern crate rand;

/*
 * The StatemapInput* types denote the structure of the concatenated JSON
 * in the input file.
 */
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StatemapInputState {
    color: Option<String>,                  // color for state, if any
    value: usize,                           // value for state
}

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StatemapInputDatum {
    #[serde(deserialize_with = "datum_time_from_string")]
    time: u64,                              // time of this datum
    entity: String,                         // name of entity
    state: u32,                             // state entity is in at time
    tag: Option<String>,                    // tag for this state, if any
}

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StatemapInputDescription {
    entity: String,                         // name of entity
    description: String,                    // description of entity
}

#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
#[serde(deny_unknown_fields)]
struct StatemapInputMetadata {
    start: Vec<u64>,
    title: String,
    host: Option<String>,
    entityKind: Option<String>,
    states: HashMap<String, StatemapInputState>,
}

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StatemapInputEvent {
    time: String,                           // time of this datum
    entity: String,                         // name of entity
    event: String,                          // type of event
    target: Option<String>,                 // target for event, if any
}

#[derive(Deserialize, Debug)]
struct StatemapInputTag {
    state: u32,                             // state for this tag
    tag: String,                            // tag itself
}

#[derive(Copy,Clone,Debug)]
pub struct Config {
    pub maxrect: u64,                       // maximum number of rectangles
    pub abstime: bool,                      // time is absolute, not relative
    pub begin: i64,                         // absolute/relative time to begin
    pub end: i64,                           // absolute/relative time to end
    pub notags: bool,                       // do not include tags
}

/*
 * These fields are dropped directly into the SVG.
 */
#[derive(Debug,Serialize)]
#[allow(non_snake_case)]
pub struct StatemapSVGConfig {
    pub stripHeight: u32,
    pub legendWidth: u32,
    pub tagWidth: u32,
    pub stripWidth: u32,
    pub background: String,
    pub sortby: Option<String>,
    pub stacksortby: Option<String>,
}

#[derive(Copy,Clone,Debug)]
struct StatemapColor {
    color: Color,                           // underlying color
}

#[derive(Debug)]
struct StatemapRect {
    start: u64,                             // nanosecond offset
    duration: u64,                          // nanosecond duration
    weight: u64,                            // my weight + neighbors
    states: Vec<u64>,                       // time spent in each state
    prev: Option<u64>,                      // previous rectangle
    next: Option<u64>,                      // next rectangle
    tags: Option<HashMap<usize, u64>>,      // tags, if any
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct StatemapRectWeight {
    weight: u64,                            // weight for this rect
    start: u64,                             // start time for this rect
    entity: usize,                          // entity for this rect
}

#[derive(Default,Clone,PartialEq,Eq,Debug,Serialize)]
struct StatemapState {
    name: String,                           // name of this state
    value: usize,                           // value for this state
    color: Option<String>,                  // color of this state, if any
}

#[derive(Debug)]
struct StatemapEntity {
    name: String,                           // name of this entity
    id: usize,                              // identifier
    description: Option<String>,            // description, if any
    last: Option<u64>,                      // last start time
    start: Option<u64>,                     // current start time
    state: Option<u32>,                     // current state
    tag: Option<usize>,                     // current tag, if any
    rects: HashMap<u64, RefCell<StatemapRect>>, // rectangles for this entity
}

#[derive(Debug)]
pub struct Statemap {
    config: Config,                         // configuration
    metadata: Option<StatemapInputMetadata>, // in-stream metadata
    nrecs: u64,                             // number of records
    nevents: u64,                           // number of events
    entities: HashMap<String, StatemapEntity>, // hash of entities
    states: Vec<StatemapState>,             // vector of valid states
    byid: Vec<String>,                      // entities by ID
    byweight: BTreeSet<StatemapRectWeight>, // rectangles by weight
    tags: HashMap<(u32, String), (Value, usize)>, // tags, if any
    begin: u64,                             // begin time, as ns since epoch
    end: u64,                               // end time, as ns since epoch
    last: u64,                              // last time seen
}

#[derive(Debug)]
pub struct StatemapError {
    errmsg: String
}

#[derive(Serialize)]
#[allow(non_snake_case)]
struct StatemapSVGGlobals<'a> {
    begin: i64,
    end: i64,
    entityPrefix: String,
    pixelHeight: u32,
    pixelWidth: u32,
    totalHeight: u32,
    timeWidth: u64,
    lmargin: u32,
    tmargin: u32,
    smargin: u32,
    states: &'a Vec<StatemapState>,
    start: &'a Vec<u64>,
    entityKind: &'a str,
}

#[derive(Serialize)]
#[allow(non_snake_case)]
struct StatemapSVGLocals<'a> {
    offset: i64,
    states: &'a Vec<StatemapState>,
    entityKind: &'a str,
    title: String,
}

pub struct StatemapSVG<'a> {
    config: &'a StatemapSVGConfig,
}

use std::fs::File;
use std::str;
use std::error::Error;
use std::fmt;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::BTreeSet;
use std::str::FromStr;
use std::cell::RefCell;
use std::cmp;
use std::path::Path;

use self::memmap::MmapOptions;
use self::palette::{Srgb, Color, Mix};
use self::serde_json::Value;

impl Default for Config {
    fn default() -> Config {
        Config { 
            maxrect: 25000,
            begin: 0,
            end: 0,
            notags: false,
            abstime: false,
        }
    }
}

impl Default for StatemapSVGConfig {
    fn default() -> StatemapSVGConfig {
        StatemapSVGConfig {
            stripHeight: 10,
            legendWidth: 138,
            stripWidth: 862,
            tagWidth: 250,
            background: "#f0f0f0".to_string(),
            sortby: None,
            stacksortby: None,
        }
    }
}

impl StatemapError {
    fn new(msg: &str) -> StatemapError {
        StatemapError { errmsg: msg.to_string() }
    }
}

impl fmt::Display for StatemapError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.errmsg)
    }
}

impl Error for StatemapError {
    fn description(&self) -> &str {
        &self.errmsg
    }
}

impl FromStr for StatemapColor {
    type Err = StatemapError;

    fn from_str(name: &str) -> Result<StatemapColor, StatemapError> {
        let named = palette::named::from_str(name);

        match named {
            Some(color) => {
                let rgb = Srgb::<f32>::from_format(color);

                return Ok(StatemapColor {
                    color: rgb.into_format().into_linear().into()
                });
            }
            None => {}
        }

        if name.len() == 7 && name.chars().next().unwrap() == '#' {
            let r = u8::from_str_radix(&name[1..3], 16);
            let g = u8::from_str_radix(&name[3..5], 16);
            let b = u8::from_str_radix(&name[5..7], 16);

            if r.is_ok() && g.is_ok() && b.is_ok() {
                let rgb = Srgb::new(r.unwrap(), g.unwrap(), b.unwrap());

                return Ok(StatemapColor {
                    color: rgb.into_format().into_linear().into()
                });
            }
        }

        return Err(StatemapError { 
            errmsg: format!("\"{}\" is not a valid color", name)
        });
    }
}

impl fmt::Display for StatemapColor {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let rgb = Srgb::from_linear(self.color.into()).into_components();

        write!(f, "rgb({}, {}, {})", (rgb.0 * 256.0) as u8,
            (rgb.1 * 256.0) as u8, (rgb.2 * 256.0) as u8)
    }
}

impl StatemapColor {
    fn random() -> Self {
        let rgb = Srgb::new(rand::random::<u8>(), rand::random::<u8>(),
            rand::random::<u8>());

        StatemapColor {
            color: rgb.into_format().into_linear().into()
        }
    }

    fn _mix(&self, other: &Self, ratio: f32) -> Self {
        StatemapColor {
            color: self.color.mix(&other.color, ratio)
        }
    }

    fn mix_nonlinear(&self, other: &Self, ratio: f32) -> Self {
        let lhs = Srgb::from_linear(self.color.into()).into_components();
        let rhs = Srgb::from_linear(other.color.into()).into_components();

        let recip = 1.0 - ratio;

        let rgb = Srgb::<f32>::new(lhs.0 as f32 * recip + rhs.0 as f32 * ratio,
            lhs.1 as f32 * recip + rhs.1 as f32 * ratio,
            lhs.2 as f32 * recip + rhs.2 as f32 * ratio);

        StatemapColor {
            color: rgb.into_format().into_linear().into()
        }
    }
}

impl StatemapRect {
    fn new(start: u64, duration: u64, state: u32, nstates: u32) -> Self {
        let mut r = StatemapRect {
            start: start,
            duration: duration,
            states: vec![0; nstates as usize],
            prev: None,
            next: None,
            weight: duration,
            tags: None,
        };

        r.states[state as usize] = duration;
        r
    }
}

fn subsume_tags(stags: &mut HashMap<usize, u64>,
    vtags: &mut HashMap<usize, u64>)
{
    for (id, duration) in vtags.drain() {
        if let Some(d) = stags.get_mut(&id) {
            *d += duration;
            continue;
        }

        stags.insert(id, duration);
    }
}

impl StatemapEntity {
    fn new(name: &str, id: usize) -> Self {
        StatemapEntity {
            name: name.to_string(),
            start: None,
            description: None,
            last: None,
            state: None,
            tag: None,
            rects: HashMap::new(),
            id: id,
        }
    }

    fn newrect(&mut self, end: u64, nstates: u32)
        -> (Option<(u64, u64, u64)>, (u64, u64))
    {
        let start = self.start.unwrap();
        let state = self.state.unwrap();
        let lhs: Option<(u64, u64, u64)>;
        let rhs: (u64, u64);
        let mut rect = StatemapRect::new(start, end - start, state, nstates);

        match self.tag {
            Some(id) => {
                let mut hash: HashMap<usize, u64> = HashMap::new();
                hash.insert(id, end - start);
                rect.tags = Some(hash);
            }
            _ => {}
        }

        rect.prev = self.last;

        match self.last {
            Some(last) => {
                let mut lrect = self.rects.get(&last).unwrap().borrow_mut();
                let old = lrect.weight;

                lrect.next = Some(start);
                rect.weight += lrect.duration;
                lrect.weight += rect.duration;

                lhs = Some((lrect.start, old, lrect.weight));
            }
            _ => { lhs = None; }
        }

        rhs = (rect.start, rect.weight);
        self.rects.insert(start, RefCell::new(rect));
        (lhs, rhs)
    }

    fn addto(&mut self, rect: u64, delta: u64) -> u64 {
        let mut r = self.rects.get(&rect).unwrap().borrow_mut();
        let old = r.weight;

        r.weight += delta;
        old
    }

    fn subsume(&mut self, victim: u64)
        -> ((Option<u64>, u64), (u64, u64), (u64, u64), (Option<u64>, u64))
    {
        let mut last = self.last;
        let subsumed: u64;
        let rval;

        /*
         * We return three weights that need to be adjusted: that of the
         * rectangle to the left (post-subsume), that of the rectangle to
         * the right (post-subsume) and that of the center rectangle.  Each
         * of these adjustments is described as a start plus a weight to be
         * added -- and all three are returned as a tuple that also includes
         * the subsumed rectangle that needs to be removed.
         */
        let ldelta: (Option<u64>, u64);
        let cdelta: (u64, u64);
        let rdelta: (Option<u64>, u64);

        /*
         * We create a scope here to help out the borrow checker in terms of
         * knowing that our immutable borrow of self.rects is being dropped
         * before our mutable borrow of it, below.
         */
        {
            let left: &RefCell<StatemapRect>;
            let right: &RefCell<StatemapRect>;

            /*
             * We create a scope here to allow the borrow of the victim
             * cell fall out of scope as we may need it to be mutable, below.
             */
            {
                let vcell = self.rects.get(&victim).unwrap();
                let v = vcell.borrow();

                match (v.prev, v.next) {
                    (None, None) => panic!("nothing to subsume"),
                    (Some(prev), None) => {
                        left = self.rects.get(&prev).unwrap();
                        right = vcell;

                        let lref = left.borrow();
                        ldelta = (lref.prev, v.duration);
                        cdelta = (lref.start, 0);
                        rdelta = (None, 0);
                    }
                    (None, Some(next)) => {
                        left = vcell;
                        right = self.rects.get(&next).unwrap();

                        /*
                         * We want the weight of the remaining (center)
                         * rectangle to be the weight of our right rectangle;
                         * to express this as a delta, we express it as the
                         * difference between the two.
                         */
                        let rref = right.borrow();
                        ldelta = (None, 0);
                        cdelta = (v.start, rref.weight - v.weight);
                        rdelta = (rref.next, v.duration);
                    }
                    (Some(prev), Some(next)) => {
                        /*
                         * We want whichever of our neighboring rectangles is
                         * shorter to subsume us.
                         */
                        let l = self.rects.get(&prev).unwrap();
                        let r = self.rects.get(&next).unwrap();

                        let lref = l.borrow();
                        let rref = r.borrow();

                        if lref.duration < rref.duration {
                            left = l;
                            right = vcell;

                            ldelta = (lref.prev, v.duration);
                            cdelta = (lref.start, v.weight -
                                (lref.duration + v.duration));
                            rdelta = (Some(rref.start), lref.duration);
                        } else {
                            left = vcell;
                            right = r;

                            ldelta = (Some(lref.start), rref.duration);
                            cdelta = (v.start, rref.weight -
                                (rref.duration + v.duration));
                            rdelta = (rref.next, v.duration);
                        }
                    }
                }
            }

            let mut s = left.borrow_mut();
            let mut v = right.borrow_mut();

            s.next = v.next;

            /*
             * Set our subsumed next rectangle's previous to point back to us
             * rather than the subsumed rectangle.
             */
            match s.next {
                Some(next) => {
                    self.rects.get(&next).unwrap()
                        .borrow_mut().prev = Some(s.start);
                }
                None => {
                    last = Some(s.start);
                }
            }

            /*
             * Add our duration, and then sum the value in each of the states.
             */
            s.duration += v.duration;

            for i in 0..v.states.len() {
                s.states[i] += v.states[i];
            }

            /*
             * If our victim has tags, we need to fold them in.
             */
            if v.tags.is_some() && s.tags.is_none() {
                s.tags = Some(HashMap::new());
            }

            match s.tags {
                Some(ref mut stags) => {
                    match v.tags {
                        Some(ref mut vtags) => { subsume_tags(stags, vtags); },
                        None => {}
                    }
                },
                None => {}
            }

            subsumed = v.start;
            rval = (v.start, v.weight);
        }

        /*
         * Okay, we're done subsuming! We can remove the subsumed rectangle.
         */
        self.rects.remove(&subsumed);
        self.last = last;

        (ldelta, cdelta, rval, rdelta)
    }

    #[must_use]
    fn apply(&mut self, deltas: ((Option<u64>, u64),
        (u64, u64), (u64, u64), (Option<u64>, u64))) ->
        Vec<(u64, u64, Option<u64>)>
    {
        let mut updates: Vec<(u64, u64, Option<u64>)> = vec![];

        /*
         * Handle the left delta.
         */
        match (deltas.0).0 {
            Some(rect) => {
                let delta = (deltas.0).1;
                updates.push((rect, self.addto(rect, delta), Some(delta)));
            }
            None => {}
        }

        /*
         * Handle the center delta.
         */
        let rect = (deltas.1).0;
        let delta = (deltas.1).1;
        updates.push((rect, self.addto(rect, delta), Some(delta)));

        /*
         * Handle the subsumed rectangle by pushing a delta update of None.
         */
        updates.push(((deltas.2).0, (deltas.2).1, None));

        /*
         * And finally, the right delta.
         */
        match (deltas.3).0 {
            Some(rect) => {
                let delta = (deltas.3).1;
                updates.push((rect, self.addto(rect, delta), Some(delta)));
            }
            None => {}
        }

        updates
    }

    fn output_svg(&self, id: usize, begin: i64, config: &StatemapSVGConfig,
        globals: &StatemapSVGGlobals, locals: &StatemapSVGLocals,
        colors: &Vec<StatemapColor>, y: u32) -> Vec<String>
    {
        let rect_width = |rect: &StatemapRect| -> f64 {
            /*
             * We add a fuzz factor to our width to assure it will always be
             * nearly (but not quite!) half a pixel wider than it should be.
             * This assures that none of the background (which is deliberately a
             * bright color) comes through at the border of rectangles, without
             * losing any accuracy (the next rectangle will tile over ours at
             * an unadjusted offset).
             */
            ((rect.duration as f64 / globals.timeWidth as f64) *
                globals.pixelWidth as f64) + 0.4 as f64
        };

        let output_tags = |rect: &StatemapRect, datum: &mut String| {
            /*
             * If we have tags, we emit them in ID order.
             */
            if let Some(ref tags) = rect.tags {
                let mut g: Vec<(usize, u64)>;

                datum.push_str(", g: {");

                g = tags.iter()
                    .map(|(&id, &duration)| { (id, duration) })
                    .collect();

                g.sort_unstable();

                for j in 0..g.len() {
                    let ratio = g[j].1 as f64 / rect.duration as f64;
                    datum.push_str(&format!("'{}': {:.3}{}", g[j].0, ratio,
                        if j < g.len() - 1 { "," } else { "" }));
                }

                datum.push_str("}");
            }
        };

        let background = |x: f64, width: f64| {
            if width > 0.0 {
                println!(r##"<rect x="{}" y="{}" width="{}"
                    height="{}" style="fill:{}" />"##, x, y, width,
                    config.stripHeight, config.background);
            }
        };

        let mut x: f64 = 0.0;
        let mut map: Vec<i64>;
        let mut data: Vec<String> = vec![];

        map = self.rects.values().map(|r| r.borrow().start as i64).collect();
        map.sort();

        if map.len() >= 1 && map[0] > begin {
            background(0.0, ((map[0] - begin) as f64 /
                globals.timeWidth as f64) * globals.pixelWidth as f64);
        }

        println!(r##"<g id="{}{}-{}"><title>{} {}</title>"##,
            globals.entityPrefix, id, self.name, locals.entityKind, self.name);

        for i in 0..map.len() {
            let rect = self.rects.get(&(map[i] as u64)).unwrap().borrow();
            let mut state = None;
            let mut blended = false;
            let w = rect_width(&rect);

            x = ((map[i] - begin) as f64 /
                globals.timeWidth as f64) * globals.pixelWidth as f64;

            for j in 0..rect.states.len() {
                if rect.states[j] != 0 {
                    match state {
                        None => { state = Some(j) },
                        Some(_s) => {
                            blended = true;
                            break;
                        }
                    }
                }
            }

            if !blended {
                assert!(state.is_some());

                let mut datum = format!("{{ \"t\": {}, \"s\": {}", rect.start,
                    state.unwrap());

                output_tags(&rect, &mut datum);
                datum.push_str("}");
                data.push(datum);

                println!(concat!(r##"<rect x="{}" y="{}" width="{}" "##,
                    r##"height="{}" onclick="mapclick(evt, {})" "##,
                    r##"style="fill:{}" />"##), x, y, w, config.stripHeight,
                    data.len() - 1, colors[state.unwrap()]);
                x += w;

                continue;
            }

            let max = rect.states.iter().enumerate()
                .max_by(|&(_, lhs), &(_, rhs)| lhs.cmp(rhs)).unwrap().0;

            let mut color = colors[max];
            let mut datum = format!("{{ t: {}, s: {{ ", rect.start);
            let mut comma = "";
            
            for j in 0..rect.states.len() {
                if rect.states[j] == 0 {
                    continue;
                }

                let ratio = rect.states[j] as f64 / rect.duration as f64;

                datum.push_str(&format!("{}'{}': {:.3}", comma, j, ratio));
                comma = ", ";

                if j != max {
                    color = color.mix_nonlinear(&colors[j], ratio as f32);
                }
            }

            datum.push_str("}");

            output_tags(&rect, &mut datum);
            datum.push_str("}");
            data.push(datum);

            println!(concat!(r##"<rect x="{}" y="{}" width="{}" "##,
                r##"height="{}" onclick="mapclick(evt, {})" "##,
                r##"style="fill:{}" />"##), x, y, w,
                config.stripHeight, data.len() - 1, color);
            x += w;
        }

        println!("</g>");

        /*
         * Finally, add a background rectangle that covers whatever remains
         * of our width.
         */
        background(x, globals.pixelWidth as f64 - x);

        data
    }

    #[cfg(test)]
    fn print(&self, header: &str) {
        let mut v: Vec<u64>;
        let l: usize;
        
        v = self.rects.values().map(|r| r.borrow().start).collect();
        v.sort();
        l = v.len();

        for i in 0..l {
            let me = self.rects.get(&v[i]).unwrap().borrow();
            println!("{}: entity={}: [{}] {:?}: {:?}",
                header, self.id, i, v[i], me);
        }

        println!("{}: entity={}: last is {:?}", header, self.id, self.last);
    }

    #[cfg(test)]
    fn verify(&self) {
        let mut v: Vec<u64>;
        let l: usize;
        
        v = self.rects.values().map(|r| r.borrow().start).collect();
        v.sort();
        l = v.len();

        for i in 0..l {
            let me = self.rects.get(&v[i]).unwrap().borrow();
            let mut weight = me.duration;

            if i < l - 1 {
                let next = self.rects.get(&v[i + 1]).unwrap().borrow();
                assert_eq!(me.next, Some(next.start));
                assert!(me.start < next.start);
                weight += next.duration;
            } else {
                assert_eq!(me.next, None);
                assert_eq!(self.last, Some(me.start));
            }

            if i > 0 {
                let prev = self.rects.get(&v[i - 1]).unwrap().borrow();
                assert_eq!(me.prev, Some(prev.start));
                assert!(me.start > prev.start);
                weight += prev.duration;
            } else {
                assert_eq!(me.prev, None);
            }

            assert_eq!(me.weight, weight);

            if let Some(ref tags) = me.tags {
                let duration = tags.iter().fold(0,
                    |i, (_id, duration)| { i + duration });

                /*
                 * This is technically a more vigorous assertion than we can
                 * make:  we actually allow for partial tagging in that not
                 * all states must by tagged all of the time.  For the moment,
                 * though, we assert that if any states have been tagged, all
                 * have been.
                 */
                assert_eq!(duration, me.duration);
            }
        }
    }

    #[cfg(test)]
    fn subsume_apply_and_verify(&mut self, victim: u64) ->
        Vec<(u64, u64, Option<u64>)>
    {
        println!("=== Subsuming {}", victim);
        self.print(&format!("Before subsuming {}", victim));
        self.verify();

        let tup = self.subsume(victim);
        println!("Weight delta from subsuming {}: {:?}", victim, tup);

        self.print(&format!("After subsuming {}, before applying", victim));
        let updates = self.apply(tup);

        self.print(&format!("After subsuming {}, after applying", victim));
        self.verify();
        updates
    }
}

enum Ingest {
    Success,
    EndOfFile,
}

fn try_parse<'de, T>(content: &mut &'de str)
    -> Result<Option<T>, serde_json::Error>
where
    T: serde::Deserialize<'de>
{
    let mut de = serde_json::Deserializer::from_str(*content).into_iter();
    match de.next() {
        Some(Ok(value)) => {
            *content = &content[de.byte_offset()..];
            Ok(Some(value))
        }
        Some(Err(err)) => Err(err),
        None => Ok(None),
    }
}

fn try_parse_raw<'de, T>(content: &mut &'de str)
    -> Result<Option<(T, serde_json::Value)>, serde_json::Error>
where
    T: serde::Deserialize<'de>
{
    let mut de = serde_json::Deserializer::from_str(*content).into_iter();
    let offset = de.byte_offset();

    match de.next() {
        Some(Ok(value)) => {
            let v: serde_json::Value =
                serde_json::from_str(&content[offset..de.byte_offset()])?;

            *content = &content[de.byte_offset()..];

            Ok(Some((value, v)))
        }
        Some(Err(err)) => Err(err),
        None => Ok(None),
    }
}

fn line_number(mmap: &[u8], byte_offset: usize) -> usize {
    let mut nls = mmap[..byte_offset].iter().filter(|&&b| b == b'\n').count();

    /*
     * We report the line number of the first non-whitespace character after
     * byte_offset.
     */
    for b in mmap[byte_offset..].iter() {
        if *b == b'\n' {
            nls += 1;
        }

        if !((*b as char).is_whitespace()) {
            break;
        }
    }

    nls + 1
}

/*
 * The time value is written in the input as a JSON string containing a number.
 * Deserialize just the number here without allocating memory for a String.
 */
fn datum_time_from_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s: &str = serde::Deserialize::deserialize(deserializer)?;
    match u64::from_str(s) {
        Ok(time) => Ok(time),
        Err(_) => Err(serde::de::Error::custom("illegal time value")),
    }
}

impl Statemap {
    pub fn new(config: &Config) -> Self {
        Statemap {
            config: *config,
            nrecs: 0,
            nevents: 0,
            entities: HashMap::new(),
            states: Vec::new(),
            byid: Vec::new(),
            byweight: BTreeSet::new(),
            metadata: None,
            tags: HashMap::new(),
            begin: 0,
            end: 0,
            last: 0,
        }
    }

    fn err<T>(&self, msg: &str) -> Result<T, Box<dyn Error>>  {
        Err(Box::new(StatemapError::new(msg)))
    }

    fn entity_lookup(&mut self, name: &str) -> &mut StatemapEntity {
        /*
         * The lack of non-lexical lifetimes causes this code to be a bit
         * gnarlier than it should really have to be.
         */
        if self.entities.contains_key(name) {
            return match self.entities.get_mut(name) {
                Some(entity) => { entity },
                None => unreachable!()
            };
        }

        let entity = StatemapEntity::new(name, self.byid.len());
        self.byid.push(name.to_string());

        self.entities.insert(name.to_string(), entity);
        self.entities.get_mut(name).unwrap()
    }

    fn tag_lookup(&mut self, state: u32, tagr: &Option<String>)
        -> Option<usize>
    {
        if self.config.notags {
            return None;
        }

        match *tagr {
            Some(ref tag) => {
                let id;

                match self.tags.get(&(state, tag.to_string())) {
                    Some(( _value, idr)) => { return Some(*idr); },
                    None => { id = self.tags.len(); }
                }

                let value = json!({ "state": state, "tag": tag.to_string() });

                self.tags.insert((state, tag.to_string()), (value, id));
                Some(id)
            },
            None => None
        }
    }

    /*
     * Takes a vector of updates to apply to our byweight tree as well as a
     * template rectangle weight and applies the updates.
     */
    fn apply(&mut self, updates: Vec<(u64, u64, Option<u64>)>,
        rweight: &mut StatemapRectWeight)
    {
        for i in 0..updates.len() {
            rweight.start = updates[i].0;
            rweight.weight = updates[i].1;

            self.byweight.remove(rweight);

            match updates[i].2 {
                Some(delta) => {
                    rweight.weight += delta;
                    self.byweight.insert(*rweight);
                }
                None => {}
            }
        }
    }

    /*
     * Subsumes the rectangle of least weight, applies the deltas to the
     * entity corresponding to that rectangle, and then applies the
     * resulting rectangle weight updates.
     */
    fn trim(&mut self) {
        let mut remove: StatemapRectWeight;
        let updates;

        remove = *self.byweight.iter().next().unwrap();
        self.byweight.remove(&remove);
        
        /*
         * We need a scope here to help the compiler out with respect to
         * our use of entity.
         */
        {
            let name = &self.byid[remove.entity];
            let entity = self.entities.get_mut(name).unwrap();

            if entity.rects.len() == 1 {
                /*
                 * If this entity only has one rectangle, than there is
                 * nothing to subsume; we simply return.  (This weight has
                 * already been removed, so we won't find it again until
                 * another rectangle is added for this entity.)
                 */
                return;
            }

            let deltas = entity.subsume(remove.start);
            updates = entity.apply(deltas);
        }

        self.apply(updates, &mut remove);
    }

    #[must_use]
    fn sort(&self, sortby: Option<usize>) -> Vec<usize>
    {
        let mut v: Vec<(u64, &String, usize)>;

        let values = self.entities.values();

        match sortby {
            None => { v = values.map(|e| (0, &e.name, e.id)).collect(); },
            Some(state) => {
                v = values.map(|e| {
                    let ttl = e.rects.values().fold(0, |i, r| {
                        i + r.borrow().states[state]
                    });

                    (ttl, &e.name, e.id)
                }).collect();
            }
        }

        v.sort_by(|&a, &b| {
            let result = b.0.cmp(&a.0);

            if result == cmp::Ordering::Equal {
                natord::compare(a.1, b.1)
            } else {
                result
            }
        });

        v.iter().map(|e| e.2).collect()
    }

    /*
     * Return the total time spent across all entities and all rectangles
     * in a particular state, for purposes of assigning a weight to the
     * statemap itself.
     */
    fn weight(&self, state: usize) -> u64
    {
        self.entities.values().fold(0, |ttl, e| {
            ttl + e.rects.values().fold(0, |i, r| {
                i + r.borrow().states[state]
            })
        })
    }

    #[cfg(test)]
    fn verify(&self) {
        /*
         * First, verify each of the entities.
         */
        for entity in self.entities.values() {
            entity.verify();
        }

        /*
         * Verify that each rectangle in each entity can be found in our
         * byweight set -- and that the weights match.
         */
        for entity in self.entities.values() {
            for cell in entity.rects.values() {
                let rect = cell.borrow();

                let rweight = StatemapRectWeight {
                    entity: entity.id,
                    weight: rect.weight,
                    start: rect.start
                };

                assert!(self.byweight.contains(&rweight) ||
                    entity.rects.len() == 1 ||
                    Some(rect.start) == entity.last ||
                    rect.next == entity.last);
            }
        }

        let mut present = HashSet::new();

        /*
         * Verify that each entity is valid that each entity/start tuple is
         * present exactly once.
         */
        for rweight in self.byweight.iter() {
            let name = &self.byid[rweight.entity];
            let tup = (rweight.entity, rweight.start);

            assert!(self.entities.get(name).is_some());

            let entity = self.entities.get(name).unwrap();

            assert!(entity.rects.get(&rweight.start).is_some());
            assert!(!present.contains(&tup));
            present.insert(tup);
        }
    }

    #[cfg(test)]
    fn subsume_apply_and_verify(&mut self, what: &str, victim: u64) {
        let id: usize;
        let mut weight: Option<u64> = None;
        let updates;

    {
        let entity = self.entity_lookup(what);
            updates = entity.subsume_apply_and_verify(victim);
            id = entity.id;
        }

        /*
         * Before we verify, remove the victim -- if it wasn't actually
         * to be removed, we'll add it back when we apply the updates.
         */
        for rweight in self.byweight.iter() {
            if rweight.entity == id && rweight.start == victim {
                assert!(weight.is_none());
                weight = Some(rweight.weight);
            }
        }

        assert!(weight.is_some());

        let mut rweight = StatemapRectWeight {
            entity: id, weight: 0, start: 0
        };

        self.apply(updates, &mut rweight);
        self.print(&format!("After subsuming {} from {}", victim, what)); 
        self.verify();
    }

    #[cfg(test)]
    fn print(&self, header: &str) {
        println!("{}: by weight: {:?}", header, self.byweight);

        for entity in self.entities.values() {
            entity.print(header);
        }

        println!("");
    }

    #[cfg(test)]
    fn get_rects(&self, entity: &str) -> Vec<(u64, u64, Vec<u64>)> {
        let mut rval: Vec<(u64, u64, Vec<u64>)>;

        let e = self.entities.get(entity);

        match e {
            Some(entity) => {
                rval = entity.rects.values().map(|r| {
                    let rect = r.borrow();

                    (rect.start, rect.duration, rect.states.clone())
                }).collect();

                rval.sort();
            },
            None => { rval = vec![]; }
        }

        rval
    }

    /*
     * Ingest and advance `payload` past the metadata JSON object.
     */
    fn ingest_metadata(&mut self, payload: &mut &str)
        -> Result<(), Box<dyn Error>>
    {
        let metadata: StatemapInputMetadata = match try_parse(payload)? {
            None => return self.err("missing metadata payload"),
            Some(metadata) => metadata,
        };

        let nstates = metadata.states.len();
        let mut states: Vec<Option<StatemapState>> = vec![None; nstates];

        if metadata.start.len() != 2 {
            return self.err(concat!("\"start\" property must be a ",
                "two element array"));
        }

        for (key, value) in &metadata.states {
            let ndx = value.value;

            if ndx >= nstates {
                let errmsg = format!(concat!("state \"{}\" has value ({}) ",
                    "that exceeds maximum allowed value ({})"),
                    key, ndx, nstates - 1);
                return self.err(&errmsg);
            }

            if ndx < states.len() && states[ndx].is_some() {
                let errmsg = format!(concat!("state \"{}\" has value ",
                    "({}) that conflicts with state \"{}\""), key,
                    ndx, states[ndx].as_ref().unwrap().name);

                return self.err(&errmsg);
            }

            states[ndx] = Some(StatemapState {
                name: key.to_string(),
                value: ndx,
                color: match value.color {
                    Some(ref str) => { Some(str.to_string()) },
                    None => { None }
                }
            });
        }

        assert_eq!(self.states.len(), 0);

        /*
         * We have verified our states; now pull them into our array.
         */
        for _i in 0..nstates {
            self.states.push(states.remove(0).unwrap());
        }

        self.metadata = Some(metadata);

        Ok(())
    }

    fn ingest_end(&mut self) {
        assert!(!self.config.abstime);

        if self.config.end < 0 {
            return;
        }

        let mut end = self.last;
        let begin = self.config.begin;

        /*
         * If we've been given an ending time and it's less than the last
         * rectangle for which we have state, we'll use that.
         */
        if self.config.end != 0 && self.config.end < end as i64 {
            end = self.config.end as u64;
        }

        let nstates = self.states.len() as u32;

        for entity in self.entities.values_mut() {
            match entity.start {
                Some(start) if start < end => {
                    /*
                     * If our start time is less than our begin time then
                     * this entity must be in a single state for our entire
                     * specified time -- and we move our start time up to our
                     * begin time (and assert that we have no rectangles).
                     */
                    if begin != 0 && (start as i64) < begin {
                        assert!(entity.rects.is_empty());
                        entity.start = Some(begin as u64);
                    }

                    /*
                     * We are adding a rectangle, but because we are now done
                     * with ingestion, we are not updating the rectangle weight
                     * tree and we are not going to subsume any rectangles; we
                     * can safely ignore the return value.
                     */
                    entity.newrect(end, nstates);

                    /*
                     * Even though we expect no other ingestion, we set our
                     * last to allow for state to be verified.
                     */
                    entity.last = entity.start;
                },
                _ => {}
            }
        }

        let metadata = self.metadata.as_ref().unwrap();
        let start = (metadata.start[0] * 1_000_000_000) + metadata.start[1];
        self.begin = (self.config.begin + start as i64) as u64;
        self.end = cmp::max(end, self.config.end as u64) + start;
    }

    /*
     * Ingest and advance `payload` past one JSON object datum.
     */
    fn ingest_datum(&mut self, payload: &mut &str)
        -> Result<Ingest, Box<dyn Error>>
    {
        match try_parse::<StatemapInputDatum>(payload) {
            Ok(None) => return Ok(Ingest::EndOfFile),
            Ok(Some(datum)) => {
                let time: u64 = datum.time;
                let nstates: u32 = self.states.len() as u32;

                self.last = time;

                /*
                 * If the time of this datum is after our specified end time,
                 * we have nothing further to do to process it.
                 */
                if self.config.end != 0 && time as i64 > self.config.end {
                    return Ok(Ingest::Success);
                }

                if datum.state >= nstates {
                    return self.err("illegal state value");
                }

                let begin = self.config.begin;
                let mut errmsg: Option<String> = None;
                let mut insert: Option<StatemapRectWeight> = None;
                let mut update: Option<(StatemapRectWeight, u64)> = None;
                let tag = self.tag_lookup(datum.state, &datum.tag);

                /*
                 * We are going to do a lookup of our entity, but this will
                 * cause us to lose our reference on self (mutable or
                 * otherwise) -- which we need to fully record any error.  To
                 * implement this absent non-lexical lifetimes, we put the
                 * entity in a lexical scope implemented with "loop" so we
                 * can break out of it on an error condition.
                 */
                loop {
                    let name = &datum.entity;
                    let entity = self.entity_lookup(name);

                    match entity.start {
                        Some(start) => {
                            if time < start {
                                errmsg = Some(format!(concat!("time {} is out",
                                    " of order with respect to prior time {}"),
                                    time, start));
                                break;
                            }

                            if (time as i64) > begin {
                                /*
                                 * We can now create a new rectangle for this
                                 * entity's past state.
                                 */
                                if begin > 0 && start < (begin as u64) {
                                    entity.start = Some(begin as u64);
                                }

                                let rval = entity.newrect(time, nstates);
                                entity.last = entity.start;

                                match rval.0 {
                                    Some(rect) => {
                                        update = Some((StatemapRectWeight {
                                            weight: rect.1,
                                            start: rect.0,
                                            entity: entity.id
                                        }, rect.2));
                                    }
                                    None => {}
                                }

                                insert = Some(StatemapRectWeight {
                                    weight: (rval.1).1,
                                    start: (rval.1).0,
                                    entity: entity.id
                                });
                            }
                        }
                        None => {}
                    }

                    entity.start = Some(time);
                    entity.state = Some(datum.state);
                    entity.tag = tag;
                    break;
                }

                if errmsg.is_some() {
                    return self.err(&errmsg.unwrap());
                }

                if update.is_some() {
                    let mut rweight = update.unwrap().0;
                    self.byweight.remove(&rweight);
                    rweight.weight = update.unwrap().1;
                    self.byweight.insert(rweight);
                }

                if insert.is_some() {
                    self.byweight.insert(insert.unwrap());
                }

                return Ok(Ingest::Success);
            }
            Err(_) => {}
        }

        match try_parse::<StatemapInputDescription>(payload) {
            Ok(None) => return Ok(Ingest::EndOfFile),
            Ok(Some(datum)) => {
                let entity = self.entity_lookup(&datum.entity);
                entity.description = Some(datum.description.to_string());

                return Ok(Ingest::Success);
            }
            Err(_) => {}
        }

        match try_parse::<StatemapInputEvent>(payload) {
            Ok(None) => return Ok(Ingest::EndOfFile),
            Ok(Some(_datum)) => {
                /*
                 * Right now, we don't do anything with events -- but the
                 * intent is to be able to render these in the statemap, so
                 * we also don't reject them.
                 */
                self.nevents += 1;

                return Ok(Ingest::Success);
            }
            Err(_) => {}
        }

        match try_parse_raw::<StatemapInputTag>(payload) {
            Ok(None) => return Ok(Ingest::EndOfFile),
            Ok(Some((datum, value))) => {
                if self.config.notags {
                    return Ok(Ingest::Success);
                }

                /*
                 * We allow tags to be redefined, so we need to first lookup
                 * our tag to see if it exists -- and if it does, we need
                 * to use the existing ID.
                 */
                let id;

                match self.tags.get(&(datum.state, datum.tag.to_string())) {
                    Some((_value, idr)) => { id = *idr }
                    None => { id = self.tags.len() }
                };

                self.tags.insert((datum.state, datum.tag), (value, id));

                return Ok(Ingest::Success);

            }
            Err(_) => {}
        }

        self.err("unrecognized payload")
    }

    pub fn ingest(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
        let file = File::open(filename)?;
        let mut nrecs = 0;

        /*
         * Unsafe because Rust cannot enforce that the underlying data on
         * filesystem is not mutated while our program contains a &[u8]
         * reference to it. Mutating the file would result in undefined
         * behavior.
         */
        let mmap = unsafe { MmapOptions::new().map(&file)? };
        let mut contents = str::from_utf8(&mmap[..])?;
        let len = contents.len();

        self.ingest_metadata(&mut contents)?;

        /*
         * If our time was presented as absolute time, we will now convert it
         * to be relative to our (now known) start time.
         */
        if self.config.abstime {
            let metadata = self.metadata.as_ref().unwrap();
            let start = (metadata.start[0] * 1_000_000_000 +
                metadata.start[1]) as i64;

            self.config.begin -= start;
            self.config.end -= start;
            self.config.abstime = false;
        }

        /*
         * Now rip through our data pulling out concatenated JSON payloads.
         */
        loop {
            match self.ingest_datum(&mut contents) {
                Ok(Ingest::Success) => nrecs += 1,
                Ok(Ingest::EndOfFile) => break,
                Err(err) => {
                    /*
                     * Lazily compute the line number for our error message.
                     */
                    let remaining_len = contents.len();
                    let byte_offset = len - remaining_len;
                    let line = line_number(&mmap, byte_offset);
                    let message =
                        format!("illegal datum on line {}: {}", line, err);
                    return self.err(&message);
                }
            }

            while self.byweight.len() >= self.config.maxrect as usize {
                self.trim();
            }
        }
        
        self.ingest_end();

        eprintln!("{}: {} records processed, {} rectangles",
            Path::new(filename).file_name().unwrap().to_string_lossy(),
            nrecs, self.byweight.len());
        Ok(())
    }

    pub fn timebounds(&self) -> (u64, u64) {
        (self.begin, self.end)
    }

    fn output_defs(&self) {
        /*
         * Provide an "entities" member that has the descriptions for each
         * entity, if they have one.  Yes, this is a little goofy -- it
         * would make much more sense to have a "descriptions" member that
         * consists of strings named by entity -- but we're doing this for
         * the sake of compatibility with the legacy implementation, however
         * dubious..
         */
        println!("entities: {{");

        let mut comma = "";

        for entity in self.entities.values() {
            let val = match entity.description {
                Some(ref description) => {
                    format!("description: \"{}\"", description)
                }
                _ => { "".to_string() }
            };

            println!("    {} \"{}\": {{ {} }}", comma, entity.name, val);
            comma = ",";
        }

        println!("}}");

        if self.tags.len() > 0 {
            /*
             * Pull our tags into a Vec so we can sort them and emit them in
             * array order.
             */
            let mut tags: Vec<(usize, u32, &str)> = vec![];

            for ((state, tag), (_value, id)) in self.tags.iter() {
                tags.push((*id, *state, tag));
            }

            tags.sort_unstable();

            println!(", tags: [");

            for i in 0..tags.len() {
                let (value, id) =
                    self.tags.get(&(tags[i].1, tags[i].2.to_string())).unwrap();

                assert_eq!(i, *id);
                println!("{}{}", serde_json::to_string_pretty(value).unwrap(),
                    if i < tags.len() - 1 { "," } else { "" });
            }

            println!("]");
        }
    }

    fn output_svg(&self, id: usize, config: &StatemapSVGConfig,
        globals: &StatemapSVGGlobals,
        colors: &Vec<StatemapColor>) -> Result<(), Box<dyn Error>>
    {
        let output_data = |data: &HashMap<&String, Vec<String>>| {
            println!("\"data\": {{ ");
            let mut comma = "";

            for entity in data.keys() {
                println!("{}\"{}\": [", comma, entity);

                let datum = data.get(entity).unwrap();

                if datum.len() > 0 {
                    for i in 0..datum.len() - 1 {
                        println!("{},", datum[i]);
                    }

                    println!("{}", datum[datum.len() - 1]);
                }

                println!("]");
                comma = ",";
            }

            println!(r##"}},"##);
        };

        let metadata = match self.metadata {
            Some(ref metadata) => { metadata }
            _ => { return self.err("metadata not found in data stream"); }
        };

        /*
         * Sort our entities, by whatever criteria has been specified.
         */
        let sort = match config.sortby {
            None => None,
            Some(ref sortby) => {
                if metadata.states.contains_key(sortby) {
                    Some(metadata.states.get(sortby).unwrap().value)
                } else {
                    if sortby == "entity" {
                        /*
                         * A state of "entity" denotes that we should sort
                         * by entity name.
                         */
                        None
                    } else {
                        return self.err(&format!(concat!("cannot sort by ",
                            "state \"{}\": no such state"), sortby));
                    }
                }
            }
        };

        let entities = self.sort(sort);

        println!(r##"<g id="statemap-{}" transform="matrix(1 0 0 1 0 0)">"##,
            id);

        let mut y = 0;
        let mut data = HashMap::new();
        let mut title = metadata.title.clone();

        if let Some(ref host) = metadata.host {
            title.push_str(&format!(" on {}", host));
        }

        let locals = StatemapSVGLocals {
            offset: self.config.begin - globals.begin,
            states: &self.states,
            entityKind: match metadata.entityKind {
                Some(ref kind) => { kind }
                None => { "Entity" }
            },
            title: title,
        };

        for e in entities {
            let entity = self.entities.get(self.byid.get(e).unwrap()).unwrap();
            data.insert(&entity.name, entity.output_svg(id,
                self.config.begin, config, globals, &locals, &colors, y));
            y += config.stripHeight;
        }

        println!("</g>");

        /*
         * Finally, output our element in the global statemaps array.
         */
        println!("<defs>");
        println!(r##"<script type="application/ecmascript"><![CDATA["##);

        let str = serde_json::to_string_pretty(&locals).unwrap();

        println!("g_statemaps[{}] = {{\n{},", id, &str[2..str.len() - 2]);

        output_data(&data);
        self.output_defs();

        println!(r##"}} ]]></script></defs>"##);

        Ok(())
    }
}

impl<'a> StatemapSVG<'a> {
    pub fn new(config: &'a StatemapSVGConfig) -> Self {
        StatemapSVG {
            config: config
        }
    }

    fn output_defs(&self, globals: &StatemapSVGGlobals)
    {
        println!("<defs>");

        println!("<script type=\"application/ecmascript\"><![CDATA[");

        println!("var globals = {{");
        let str = serde_json::to_string_pretty(&self.config).unwrap();
        println!("{},", &str[2..str.len() - 2]);

        let str = serde_json::to_string_pretty(&globals).unwrap();
        println!("{},", &str[2..str.len() - 2]);
        println!("}}");

        /*
         * Now drop in our in-SVG code.
         */
        let lib = include_str!("statemap-svg.js");

        println!("{}\n]]></script>", lib);

        /*
         * Next up: CSS.
         */
        let css = include_str!("statemap-svg.css");

        println!("<style type=\"text/css\"><![CDATA[\n{}\n]]></style>", css);

        /*
         * And now other definitions.
         */
        let defs = include_str!("statemap-svg.defs");
        println!("{}", defs);

        println!("</defs>");
    }

    fn title(&self, statemaps: &Vec<Statemap>) -> String
    {
        /*
         * A little helper routine to generate an Oxford comma-separated
         * list.
         */
        let oxford = |v: &Vec<String>| {
            let map: HashSet<_> = v.iter().collect();

            if map.len() == 1 {
                v[0].clone()
            } else if v.len() == 2 {
                v.join(" and ")
            } else {
                (&v[0..v.len() - 1]).join(", ") + ", and " + &v[v.len() - 1]
            }
        };

        /*
         * If we have "statemap" anywhere in the title of the first statemap,
         * we assume it's a legacy title and just use that.
         */
        let metadata = statemaps[0].metadata.as_ref().unwrap();

        if metadata.title.find("tatemap").is_some() {
            return metadata.title.clone();
        }

        /*
         * Gather our titles.
         */
        let titles = statemaps.iter()
            .filter_map(|s| s.metadata.as_ref())
            .map(|m| m.title.to_string())
            .collect::<Vec<String>>();

        let mut title = vec!["Statemap of".to_string(),
            oxford(&titles), "activity".to_string()];

        let hosts = statemaps.iter()
            .filter_map(|s| s.metadata.as_ref())
            .filter_map(|m| m.host.as_ref())
            .map(|r| r.to_string())
            .collect::<Vec<String>>();

        if hosts.len() > 0 {
            title.push("on".to_string());

            if hosts.iter().collect::<HashSet<_>>().len() > 5 {
                title.push(format!("{} hosts", hosts.len()));
            } else {
                title.push(oxford(&hosts));
            }
        }

        title.join(" ")
    }

    pub fn output(&self, statemaps: &Vec<Statemap>) ->
        Result<(), Box<dyn Error>>
    {
        struct Props {
            x: u32,
            y: u32,
            height: u32,
            width: u32,
            lheight: u32,
            spacing: u32
        };

        let base = &statemaps[0];

        let output_controls = |props: &Props| {
            let width = props.width / 4;
            let mut x = 0;
            let y = 0;

            let icons = vec![
                (include_str!("./icons/arrow-left-l.svg"), "panclick(50, 0)"),
                (include_str!("./icons/zoom-in.svg"), "zoomclick(1.25)"),
                (include_str!("./icons/zoom-out.svg"), "zoomclick(0.8)"),
                (include_str!("./icons/arrow-right-l.svg"), "panclick(-50, 0)")
            ];

            println!(r##"<svg x="{}px" y="{}px" width="{}px" height="{}px">"##,
                props.x, props.y, props.width, props.height);

            for i in 0..icons.len() {
                println!(r##"<svg x="{}px" y="{}px" width="{}px" height="{}px"
                    onclick="{}"><rect x="0px" y="0px" width="{}px" 
                    height="{}px" onclick="{}" class="button" />{}</svg>"##,
                    x, y, width, width, icons[i].1,
                    width, width, icons[i].1, icons[i].0);
                x += width;
            }

            println!("</svg>");
        };

        let output_legend = |statemap: &Statemap, id: usize,
            props: &mut Props, colors: &Vec<StatemapColor>|
        {
            let x = props.x;
            let mut y = props.y;
            let height = props.lheight;
            let width = props.width;

            for state in 0..statemap.states.len() {
                println!(concat!(r##"<rect x="{}" y="{}" width="{}" "##,
                    r##"height="{}" id="statemap-legend-{}-{}" "##,
                    r##"onclick="legendclick(evt, {}, {})" "##,
                    r##"class="statemap-legend" style="fill:{}" />"##),
                    x, y, width, height, id, state, id, state, colors[state]);
                y += height + props.spacing;

                println!(concat!(r##"<text x="{}" y="{}" "##,
                    r##"class="statemap-legendlabel sansserif">{}</text>"##),
                    x + (width / 2), y, statemap.states[state].name);
                y += props.spacing;
            }

            props.y = y;
        };

        let output_tagbox = || {
            if !base.config.notags {
                println!(r##"<g id="statemap-tagbox"></g>"##);
                println!(r##"<g id="statemap-tagbox-select"></g>"##);
            }
        };

        let metadata = match base.metadata {
            Some(ref metadata) => { metadata }
            _ => { return base.err("metadata not found in data stream"); }
        };

        #[allow(non_snake_case)]
        let timeWidth = base.entities.values().fold(base.config.end,
            |latest, e| {
                match e.start {
                    Some(start) => cmp::max(latest, start as i64),
                    None => latest
               }
            }) - base.config.begin;

        assert!(timeWidth >= 0);

        let lmargin = self.config.legendWidth;
        let tmargin = 60;
        let rmargin = self.config.tagWidth;
        let smargin = self.config.stripHeight;
        let height: u32;

        let stacked = true;

        if stacked || statemaps.len() == 1 {
            let nentities = statemaps.iter().fold(0,
                |total, statemap| { total + statemap.entities.len() });

            height = nentities as u32 * self.config.stripHeight +
                tmargin + ((statemaps.len() as u32) - 1) * smargin;
        } else {
            panic!("tabbed statemaps not yet supported");
        }

        let width = self.config.stripWidth + lmargin + rmargin;

        let mut props = Props { x: 20, y: tmargin, height: 45,
            width: lmargin, lheight: 15, spacing: 10 };

        let mut sharedlegend = true;
        let mut lheight = tmargin + props.height;

        /*
         * We need to add in our legend height, but to determine this, we
         * need to see to what degree we will be sharing legends.
         */
        for i in 0..statemaps.len() {
            if i == 0 || statemaps[i].states != statemaps[i - 1].states {
                lheight += statemaps[i].states.len() as u32 *
                    (props.lheight + (props.spacing * 2));

                if i > 0 {
                    lheight += props.spacing * 2;
                    sharedlegend = false;
                }
            }
        }

        let globals = StatemapSVGGlobals {
            begin: base.config.begin,
            end: base.config.end,
            pixelWidth: self.config.stripWidth,
            pixelHeight: height - tmargin,
            totalHeight: cmp::max(height, lheight),
            timeWidth: timeWidth as u64,
            lmargin: lmargin,
            tmargin: tmargin,
            smargin: smargin,
            entityPrefix: "statemap-entity-".to_string(),
            states: &base.states,
            start: &metadata.start,
            entityKind: match metadata.entityKind {
                Some(ref kind) => { kind }
                None => { "Entity" }
            }
        };

        /*
         * Make sure that all of our colors are valid.
         */
        let mut colors: Vec<Vec<StatemapColor>> = vec![];

        for i in 0..statemaps.len() {
            for j in 0..statemaps[i].states.len() {
                colors.push(vec![]);

                match statemaps[i].states[j].color {
                    Some(ref name) => {
                        match StatemapColor::from_str(name) {
                            Ok(color) => colors[i].push(color),
                            Err(_err) => {
                                return base.err(&format!(concat!("illegal ",
                                    "color \"{}\" for state \"{}\""), name,
                                    statemaps[i].states[j].name));
                            }
                        }
                    }
                    None => colors[i].push(StatemapColor::random())
                }
            }
        }

        let sorted: Vec<usize>;

        match self.config.stacksortby {
            None => { sorted = (0..statemaps.len()).collect(); }
            Some(ref stacksortby) => {
                /*
                 * If we aren't all sharing a legend, this doesn't make sense.
                 */
                if !sharedlegend {
                    return base.err("can only stack sort like statemaps");
                }

                if !metadata.states.contains_key(stacksortby) {
                    return base.err(&format!(concat!("unknown stack sorting ",
                        "state \"{}\""), stacksortby));
                }

                let sort = metadata.states.get(stacksortby).unwrap().value;

                let mut weights = statemaps.iter()
                    .map(|s| s.weight(sort))
                    .enumerate()
                    .collect::<Vec<(usize, _)>>();

                weights.sort_by(|&(_, l), &(_, r)| r.cmp(&l));

                sorted = weights.iter().map(|&(e, _)| e).collect();
            }
        }

        println!(r##"<?xml version="1.0"?>
            <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
                "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
            <svg width="{}" height="{}"
                xmlns="http://www.w3.org/2000/svg"
                version="1.1"
                onload="init(evt)">"##, width, globals.totalHeight);

        self.output_defs(&globals);

        let mut y = tmargin;

        for i in 0..statemaps.len() {
            let statemap = &statemaps[sorted[i]];

            let height = statemap.entities.len() as u32 *
                self.config.stripHeight;

            println!(r##"<svg x="{}px" y="{}px" width="{}px" height="{}px">"##,
                lmargin, y, globals.pixelWidth, height);

            /*
             * First, we drop down a background rectangle as big as our SVG.
             * This color will be changed dynamically to be a highlight color,
             * and then rectangles can be made transparent to become
             * highlighted.
             */
            println!(concat!(r##"<rect x="0px" y="0px" width="{}px" "##,
                r##"height="{}px" fill="{}" id="statemap-{}-highlight" />"##),
                globals.pixelWidth, height, self.config.background, i);

            statemap.output_svg(i, &self.config, &globals, &colors[i])?;

            println!("</svg>");

            /*
             * The border around this statemap.
             */
            println!(r##"<polygon class="statemap-border""##);
            println!(r##"  points="{} {}, {} {}, {} {}, {} {}"/>"##,
                lmargin, y, lmargin + globals.pixelWidth, y,
                lmargin + globals.pixelWidth, y + height, lmargin, y + height);

            y += height + smargin;
        }

        println!(concat!(r##"<text x="{}" y="{}" "##,
            r##"class="statemap-title sansserif">{}</text>"##),
            lmargin + (globals.pixelWidth / 2), 16,
            self.title(statemaps));

        println!(concat!(r##"<text x="{}" y="{}" class="statemap-timelabel"##,
            r##" sansserif" id="statemap-timelabel"></text>"##),
            lmargin + (globals.pixelWidth / 2), 34);

        println!(r##"<line x1="{}" y1="{}" x2="{}" y2="{}""##,
            lmargin + 10, 40, lmargin + globals.pixelWidth - 10, 40);
        println!(r##"class="statemap-timeline" />"##);

        props.width -= (2 * props.x) + 10;

        output_controls(&props);

        props.y += props.height;

        for i in 0..statemaps.len() {
            if i == 0 || statemaps[i].states != statemaps[i - 1].states {
                output_legend(&statemaps[i], i, &mut props, &colors[i]);
            }

            if !sharedlegend {
                props.y += props.spacing * 2;
            }
        }

        output_tagbox();

        println!("</svg>");

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;
    use std::process;
    use std::fs;
    use std::io::Write;

    fn metadata(config: Option<&Config>, mut metadata: &str) -> Statemap {
        let mut statemap;

        match config {
            Some(config) => { statemap = Statemap::new(&config); },
            None => {
                let config: Config = Default::default();
                statemap = Statemap::new(&config);
            }
        }

        match statemap.ingest_metadata(&mut metadata) {
            Err(err) => { panic!("metadata incorrectly failed: {:?}", err); }
            Ok(_) => { statemap }
        }
    }

    fn minimal(config: Option<&Config>) -> Statemap {
        metadata(config, r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 0 },
                "one": {"value": 1 }
            }
        }"##)
    }

    fn data(config: Option<&Config>, data: Vec<&str>) -> Statemap {
        let mut statemap = minimal(config);

        for mut datum in data {
            match statemap.ingest_datum(&mut datum) {
                Err(err) => { panic!("data incorrectly failed: {:?}", err); }
                Ok(_) => {}
            }
        }

        statemap.ingest_end();
        statemap
    }

    fn bad_metadata(mut metadata: &str, expected: &str) {
        let config: Config = Default::default();
        let mut statemap = Statemap::new(&config);

        match statemap.ingest_metadata(&mut metadata) {
            Err(err) => {
                let errmsg = format!("{}", err);

                if errmsg.find(expected).is_none() {
                    panic!("error ('{}') did not contain '{}' as expected",
                        errmsg, expected);
                }
            },
            Ok(_) => { panic!("bad metadata succeeded!"); }
        }
    }

    fn bad_datum(operand: Option<Statemap>, mut datum: &str, expected: &str) {
        let mut statemap = match operand {
            Some(statemap) => statemap,
            None => {
                metadata(None, r##"{
                    "start": [ 0, 0 ],
                    "title": "Foo",
                    "states": {
                        "zero": {"value": 0 },
                        "one": {"value": 1 }
                    }
                }"##)
            }
        };

        match statemap.ingest_datum(&mut datum) {
            Err(err) => {
                let errmsg = format!("{}", err);

                if errmsg.find(expected).is_none() {
                    panic!("error ('{}') did not contain '{}' as expected",
                        errmsg, expected);
                }
            },
            Ok(_) => { panic!("bad datum succeeded!"); }
        }
    }

    fn statemap_ingest(statemap: &mut Statemap, raw: &str)
        -> Result<(), Box<Error>>
    {
        let mut path = env::temp_dir();
        path.push(format!("statemap.test.{}.{:p}", process::id(), statemap));

        let filename = path.to_str().unwrap();
        let mut file = File::create(filename)?;
        file.write_all(raw.as_bytes())?;

        let result = statemap.ingest(filename);

        fs::remove_file(filename)?;

        result
    }

    fn bad_statemap(raw: &str, expected: &str) {
        let config: Config = Default::default();
        let mut statemap = Statemap::new(&config);

        match statemap_ingest(&mut statemap, raw) {
            Err(err) => {
                let errmsg = format!("{}\n", err);

                if errmsg.find(expected).is_none() {
                    panic!("error ('{}') did not contain '{}' as expected",
                        errmsg, expected);
                }
            },
            Ok(_) => { panic!("bad statemap succeeded!"); }
        }
    }

    macro_rules! bad_statemap {
        ($what:expr) => ({
            bad_statemap(include_str!(concat!("../tst/tst.", $what, ".in")),
                include_str!(concat!("../tst/tst.", $what, ".err")));
        });
    }

    fn good_statemap(config: &Config, raw: &str) -> Statemap {
        let mut statemap = Statemap::new(&config);

        match statemap_ingest(&mut statemap, raw) {
            Err(err) => {
                panic!("statemap failed: {}", err);
            },
            Ok(_) => { statemap }
        }
    }

    macro_rules! good_statemap {
        ($what:expr) => ({
            let mut config: Config = Default::default();
            config.notags = false;

            good_statemap(&config,
                include_str!(concat!("../tst/tst.", $what, ".in")))
        });

        ($what:expr, $conf:expr) => ({
            good_statemap($conf,
                include_str!(concat!("../tst/tst.", $what, ".in")))
        });
    }

    #[test]
    fn good_minimal() {
        metadata(None, r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 0 }
            }
        }"##);
    }

    #[test]
    fn bad_title_missing() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "states": {
                "zero": {"value": 0 }
            }
        }"##, "missing field `title`");
    }

    #[test]
    fn bad_start_missing() {
        bad_metadata(r##"{
            "title": "Foo",
            "states": {
                "zero": {"value": 0 }
            }
        }"##, "missing field `start`");
    }

    #[test]
    fn bad_start_badval() {
        bad_metadata(r##"{
            "start": [ -1, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 0 }
            }
        }"##, "invalid value: integer `-1`");
    }

    #[test]
    fn bad_start_tooshort() {
        bad_metadata(r##"{
            "start": [ 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 0 }
            }
        }"##, "\"start\" property must be a two element array");
    }

    #[test]
    fn bad_start_toolong() {
        bad_metadata(r##"{
            "start": [ 0, 0, 3 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 0 }
            }
        }"##, "\"start\" property must be a two element array");
    }

    #[test]
    fn bad_states_missing() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo"
        }"##, "missing field `states`");
    }

    #[test]
    fn bad_states_badmap() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": 123
        }"##, "expected a map");
    }

    #[test]
    fn bad_states_value_missing() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {}
            }
        }"##, "missing field `value`");
    }

    #[test]
    fn bad_states_value_bad() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": -1 }
            }
        }"##, "invalid value: integer `-1`");
    }

    #[test]
    fn bad_states_value_skipped1() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 0 },
                "one": {"value": 2 }
            }
        }"##, "state \"one\" has value (2) that exceeds maximum");
    }

    #[test]
    fn bad_states_value_toohigh() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 1 },
                "one": {"value": 2 }
            }
        }"##, "state \"one\" has value (2) that exceeds maximum");
    }

    #[test]
    fn bad_states_value_duplicate() {
        bad_metadata(r##"{
            "start": [ 0, 0 ],
            "title": "Foo",
            "states": {
                "zero": {"value": 1 },
                "one": {"value": 1 }
            }
        }"##, "has value (1) that conflicts");
    }

    #[test]
    fn bad_line_basic() {
        bad_statemap!("bad_line_basic");
    }

    #[test]
    fn bad_line_whitespace() {
        bad_statemap!("bad_line_whitespace");
    }

    #[test]
    fn bad_line_newline() {
        bad_statemap!("bad_line_newline");
    }

    #[test]
    fn basic() {
        let statemap = metadata(None, r##"{
            "start": [ 1528417173, 255882937 ],
            "title": "Foo",
            "host": "HA8S7MRD2",
            "entityKind": "Process",
            "states": {
                "on-cpu": {"value": 0, "color": "#2e9107" },
                "off-cpu-waiting": {"value": 1, "color": "#f9f9f9" },
                "off-cpu-semop": {"value": 2, "color": "#FF5733" },
                "off-cpu-blocked": {"value": 3, "color": "#C70039" },
                "off-cpu-zfs-read": {"value": 4, "color": "#FFC300" },
                "off-cpu-zfs-write": {"value": 5, "color": "#338AFF" },
                "off-cpu-zil-commit": {"value": 6, "color": "#66FFCC" },
                "off-cpu-tx-delay": {"value": 7, "color": "#e1ff00" },
                "off-cpu-dead": {"value": 8, "color": "#E0E0E0" }
            }
        }"##);
        assert_eq!(statemap.states.len(), 9);
        assert_eq!(statemap.states[0].name, "on-cpu");
        assert_eq!(statemap.states[0].color, Some("#2e9107".to_string()));
        assert_eq!(statemap.states[1].name, "off-cpu-waiting");
        assert_eq!(statemap.states[1].color, Some("#f9f9f9".to_string()));
        assert_eq!(statemap.states[8].name, "off-cpu-dead");
    }

    #[test]
    fn basic_datum() {
        let mut _statemap = data(None, vec![
            r##"{ "time": "156683", "entity": "foo", "state": 0 }"##
        ]);
    }

    #[test]
    fn basic_description() {
        let mut statemap = data(None, vec![
            r##"{ "time": "156683", "entity": "foo", "state": 0 }"##,
            r##"{ "entity": "foo", "description": "This is a foo!" }"##
        ]);

        assert_eq!(statemap.entity_lookup("foo").description,
            Some("This is a foo!".to_string()));
    }

    #[test]
    fn bad_datum_badtime() {
        bad_datum(None, r##"
            { "time": 156683, "entity": "foo", "state": 0 }
        "##, "unrecognized payload");
    }

    #[test]
    fn bad_datum_badtime_float() {
        bad_datum(None, r##"
            { "time": "156683.12", "entity": "foo", "state": 0 }
        "##, "unrecognized payload");
    }

    #[test]
    fn bad_datum_nostate() {
        bad_datum(None, r##"
            { "time": "156683", "entity": "foo" }
        "##, "unrecognized payload");
    }

    #[test]
    fn bad_datum_badstate() {
        bad_datum(None, r##"
            { "time": "156683", "entity": "foo", "state": 200 }
        "##, "illegal state value");
    }

    #[test]
    fn bad_datum_backwards() {
        let statemap = data(None, vec![
            r##"{ "time": "156683", "entity": "foo", "state": 0 }"##
        ]);

        bad_datum(Some(statemap), r##"
            { "time": "156682", "entity": "foo", "state": 1 }
        "##, "out of order with respect to prior time");
    }

    #[test]
    fn basic_data() {
        let statemap = data(None, vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.verify();
    }

    #[test]
    fn subsume() {
        let mut statemap = data(None, vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.print("Initial load");
        statemap.verify();
        statemap.subsume_apply_and_verify("foo", 100000);
        statemap.subsume_apply_and_verify("foo", 300000);
        statemap.subsume_apply_and_verify("foo", 100000);
    }

    #[test]
    fn subsume_right() {
        let mut statemap = data(None, vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.print("Initial load");
        statemap.verify();

        statemap.subsume_apply_and_verify("foo", 500000);
        statemap.subsume_apply_and_verify("foo", 400000);
        statemap.subsume_apply_and_verify("foo", 300000);
        statemap.subsume_apply_and_verify("foo", 200000);
    }

    #[test]
    fn subsume_middle() {
        let mut statemap = data(None, vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.print("Initial load");
        statemap.verify();
        statemap.subsume_apply_and_verify("foo", 300000);
        statemap.subsume_apply_and_verify("foo", 300000);
        statemap.subsume_apply_and_verify("foo", 200000);
    }

    #[test]
    fn subsume_tagged() {
        let mut statemap = data(None, vec![
            r##"{ "time": "100", "entity": "foo", "state": 0, "tag": "a" }"##,
            r##"{ "time": "200", "entity": "foo", "state": 1, "tag": "b" }"##,
            r##"{ "time": "300", "entity": "foo", "state": 0, "tag": "c" }"##,
            r##"{ "time": "400", "entity": "foo", "state": 1, "tag": "b" }"##,
            r##"{ "time": "500", "entity": "foo", "state": 0, "tag": "a" }"##,
            r##"{ "time": "600", "entity": "foo", "state": 1, "tag": "b" }"##
        ]);

        statemap.print("Initial load");
        statemap.verify();
        statemap.subsume_apply_and_verify("foo", 100);
        statemap.subsume_apply_and_verify("foo", 300);
        statemap.subsume_apply_and_verify("foo", 100);
    }

    #[test]
    fn trim() {
        let mut statemap = data(None, vec![
            r##"{ "time": "0", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "100", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "101", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "104", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "106", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "206", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "207", "entity": "foo", "state": 0 }"##
        ]);

        statemap.print("Initial");

        statemap.trim();
        statemap.verify();
        statemap.print("After first trim");

        statemap.trim();
        statemap.verify();
        statemap.print("After second trim");

        statemap.trim();
        statemap.verify();
        statemap.print("After third trim");
    }

    #[test]
    fn trim_insert() {
        let mut statemap = data(None, vec![
            r##"{ "time": "0", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "100", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "101", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "104", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "106", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "206", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "207", "entity": "foo", "state": 0 }"##
        ]);

        statemap.print("Initial");

        statemap.trim();
        statemap.verify();
        statemap.print("After first trim");

        let mut datum = r##"{ "time": "210", "entity": "foo", "state": 1 }"##;

        assert!(statemap.ingest_datum(&mut datum).is_ok());
        statemap.verify();
        statemap.print("After insert");

        statemap.trim();
        statemap.verify();
        statemap.print("After second trim");
    }

    #[test]
    fn trim_multient() {
        let mut statemap = data(None, vec![
            r##"{ "time": "0", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "1000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "1010", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "1040", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "1060", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "2060", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "2070", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "0", "entity": "bar", "state": 0 }"##,
            r##"{ "time": "10", "entity": "bar", "state": 1 }"##,
        ]);

        statemap.print("Initial");

        statemap.trim();
        statemap.verify();
        statemap.print("After trim");
    }

    #[test]
    fn data_begin_time() {
        let mut config: Config = Default::default();
        config.begin = 200000;

        let statemap = data(Some(&config), vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.verify();
        statemap.print("Begin at 200000");

        let rects = statemap.get_rects("foo");
        assert_eq!(rects.len(), 4);
        assert_eq!(rects[0].0, 200000);
        assert_eq!(rects[0].1, 300000 - 200000);
    }

    #[test]
    fn data_begin_time_later() {
        let mut config: Config = Default::default();
        config.begin = 200001;

        let statemap = data(Some(&config), vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.verify();
        statemap.print("Begin at 200001");

        let rects = statemap.get_rects("foo");
        assert_eq!(rects.len(), 4);
        assert_eq!(rects[0].0, 200001);
        assert_eq!(rects[0].1, 300000 - 200001);
        assert_eq!((rects[0].2)[0], 0);
        assert_eq!((rects[0].2)[1], 300000 - 200001);
    }

    #[test]
    fn data_begin_end_time() {
        let mut config: Config = Default::default();
        config.begin = 250000;
        config.end = 310000;

        let statemap = data(Some(&config), vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.verify();
        statemap.print("Begin at 250000, end at 310000");

        let rects = statemap.get_rects("foo");
        assert_eq!(rects.len(), 2);
        assert_eq!(rects[0].0, 250000);
        assert_eq!(rects[0].1, 250000 - 200000);
        assert_eq!((rects[0].2)[0], 0);
        assert_eq!((rects[0].2)[1], 250000 - 200000);

        assert_eq!(rects[1].0, 300000);
        assert_eq!(rects[1].1, 310000 - 300000);
        assert_eq!((rects[1].2)[0], 310000 - 300000);
        assert_eq!((rects[1].2)[1], 0);
    }

    #[test]
    fn data_wrapped_time() {
        let mut config: Config = Default::default();
        config.begin = 250000;
        config.end = 260000;

        let statemap = data(Some(&config), vec![
            r##"{ "time": "100000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "200000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "300000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "400000", "entity": "foo", "state": 1 }"##,
            r##"{ "time": "500000", "entity": "foo", "state": 0 }"##,
            r##"{ "time": "600000", "entity": "foo", "state": 1 }"##
        ]);

        statemap.verify();
        statemap.print("Begin at 250000, end at 260000");

        let rects = statemap.get_rects("foo");
        assert_eq!(rects.len(), 1);
        assert_eq!(rects[0].0, 250000);
        assert_eq!(rects[0].1, 260000 - 250000);
        assert_eq!((rects[0].2)[0], 0);
        assert_eq!((rects[0].2)[1], 260000 - 250000);
    }

    #[test]
    fn color_named() {
        let colors = vec![
            ("aliceblue", (240, 248, 255)),
            ("antiquewhite", (250, 235, 215)),
            ("aqua", (0, 255, 255)),
            ("aquamarine", (127, 255, 212)),
            ("azure", (240, 255, 255)),
            ("beige", (245, 245, 220)),
            ("bisque", (255, 228, 196)),
            ("black", (0, 0, 0)),
            ("blanchedalmond", (255, 235, 205)),
            ("blue", (0, 0, 255)),
            ("blueviolet", (138, 43, 226)),
            ("brown", (165, 42, 42)),
            ("burlywood", (222, 184, 135)),
            ("cadetblue", (95, 158, 160)),
            ("chartreuse", (127, 255, 0)),
            ("chocolate", (210, 105, 30)),
            ("coral", (255, 127, 80)),
            ("cornflowerblue", (100, 149, 237)),
            ("cornsilk", (255, 248, 220)),
            ("crimson", (220, 20, 60)),
            ("cyan", (0, 255, 255)),
            ("darkblue", (0, 0, 139)),
            ("darkcyan", (0, 139, 139)),
            ("darkgoldenrod", (184, 134, 11)),
            ("darkgray", (169, 169, 169)),
            ("darkgreen", (0, 100, 0)),
            ("darkgrey", (169, 169, 169)),
            ("darkkhaki", (189, 183, 107)),
            ("darkmagenta", (139, 0, 139)),
            ("darkolivegreen", (85, 107, 47)),
            ("darkorange", (255, 140, 0)),
Download .txt
gitextract_6twupnbu/

├── .gitmodules
├── AUTHORS
├── Cargo.toml
├── LICENSE
├── README.md
├── contrib/
│   ├── cpu-statemap-tagged.d
│   ├── cpu-statemap.d
│   ├── io-statemap.d
│   ├── lx-cmd-statemap.d
│   ├── lx-statemap.d
│   ├── postgres-statemap.d
│   ├── postgres-zfs-statemap.d
│   ├── spa-sync-statemap.d
│   └── vdev-statemap.d
├── src/
│   ├── icons/
│   │   ├── LICENSE
│   │   └── README.md
│   ├── main.rs
│   ├── statemap-svg.css
│   ├── statemap-svg.defs
│   ├── statemap-svg.js
│   └── statemap.rs
└── tst/
    ├── tst.bad_line_basic.err
    ├── tst.bad_line_basic.in
    ├── tst.bad_line_newline.err
    ├── tst.bad_line_newline.in
    ├── tst.bad_line_whitespace.err
    ├── tst.bad_line_whitespace.in
    ├── tst.io.in
    ├── tst.tag_basic.in
    └── tst.tag_redefined.in
Download .txt
SYMBOL INDEX (123 symbols across 2 files)

FILE: src/main.rs
  function usage (line 36) | fn usage(opts: Options) {
  function parse_offset (line 41) | fn parse_offset(matches: &getopts::Matches, opt: &str) -> i64 {
  function main (line 94) | fn main() {

FILE: src/statemap.rs
  type StatemapInputState (line 18) | struct StatemapInputState {
  type StatemapInputDatum (line 25) | struct StatemapInputDatum {
  type StatemapInputDescription (line 35) | struct StatemapInputDescription {
  type StatemapInputMetadata (line 43) | struct StatemapInputMetadata {
  type StatemapInputEvent (line 53) | struct StatemapInputEvent {
  type StatemapInputTag (line 61) | struct StatemapInputTag {
  type Config (line 67) | pub struct Config {
  type StatemapSVGConfig (line 80) | pub struct StatemapSVGConfig {
  type StatemapColor (line 91) | struct StatemapColor {
    method fmt (line 282) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    method random (line 291) | fn random() -> Self {
    method _mix (line 300) | fn _mix(&self, other: &Self, ratio: f32) -> Self {
    method mix_nonlinear (line 306) | fn mix_nonlinear(&self, other: &Self, ratio: f32) -> Self {
  type StatemapRect (line 96) | struct StatemapRect {
    method new (line 323) | fn new(start: u64, duration: u64, state: u32, nstates: u32) -> Self {
  type StatemapRectWeight (line 107) | struct StatemapRectWeight {
  type StatemapState (line 114) | struct StatemapState {
  type StatemapEntity (line 121) | struct StatemapEntity {
    method new (line 353) | fn new(name: &str, id: usize) -> Self {
    method newrect (line 366) | fn newrect(&mut self, end: u64, nstates: u32)
    method addto (line 405) | fn addto(&mut self, rect: u64, delta: u64) -> u64 {
    method subsume (line 413) | fn subsume(&mut self, victim: u64)
    method apply (line 566) | fn apply(&mut self, deltas: ((Option<u64>, u64),
    method output_svg (line 609) | fn output_svg(&self, id: usize, begin: i64, config: &StatemapSVGConfig,
    method print (line 761) | fn print(&self, header: &str) {
    method verify (line 779) | fn verify(&self) {
    method subsume_apply_and_verify (line 829) | fn subsume_apply_and_verify(&mut self, victim: u64) ->
  type Statemap (line 133) | pub struct Statemap {
    method new (line 927) | pub fn new(config: &Config) -> Self {
    method err (line 944) | fn err<T>(&self, msg: &str) -> Result<T, Box<dyn Error>>  {
    method entity_lookup (line 948) | fn entity_lookup(&mut self, name: &str) -> &mut StatemapEntity {
    method tag_lookup (line 967) | fn tag_lookup(&mut self, state: u32, tagr: &Option<String>)
    method apply (line 996) | fn apply(&mut self, updates: Vec<(u64, u64, Option<u64>)>,
    method trim (line 1020) | fn trim(&mut self) {
    method sort (line 1053) | fn sort(&self, sortby: Option<usize>) -> Vec<usize>
    method weight (line 1090) | fn weight(&self, state: usize) -> u64
    method verify (line 1100) | fn verify(&self) {
    method subsume_apply_and_verify (line 1150) | fn subsume_apply_and_verify(&mut self, what: &str, victim: u64) {
    method print (line 1184) | fn print(&self, header: &str) {
    method get_rects (line 1195) | fn get_rects(&self, entity: &str) -> Vec<(u64, u64, Vec<u64>)> {
    method ingest_metadata (line 1219) | fn ingest_metadata(&mut self, payload: &mut &str)
    method ingest_end (line 1277) | fn ingest_end(&mut self) {
    method ingest_datum (line 1338) | fn ingest_datum(&mut self, payload: &mut &str)
    method ingest (line 1503) | pub fn ingest(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
    method timebounds (line 1566) | pub fn timebounds(&self) -> (u64, u64) {
    method output_defs (line 1570) | fn output_defs(&self) {
    method output_svg (line 1625) | fn output_svg(&self, id: usize, config: &StatemapSVGConfig,
  type StatemapError (line 149) | pub struct StatemapError {
    method new (line 227) | fn new(msg: &str) -> StatemapError {
    method fmt (line 233) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  type StatemapSVGGlobals (line 155) | struct StatemapSVGGlobals<'a> {
  type StatemapSVGLocals (line 173) | struct StatemapSVGLocals<'a> {
  type StatemapSVG (line 180) | pub struct StatemapSVG<'a> {
  method default (line 201) | fn default() -> Config {
  method default (line 213) | fn default() -> StatemapSVGConfig {
  method description (line 239) | fn description(&self) -> &str {
  type Err (line 245) | type Err = StatemapError;
  method from_str (line 247) | fn from_str(name: &str) -> Result<StatemapColor, StatemapError> {
  function subsume_tags (line 339) | fn subsume_tags(stags: &mut HashMap<usize, u64>,
  type Ingest (line 848) | enum Ingest {
  function try_parse (line 853) | fn try_parse<'de, T>(content: &mut &'de str)
  function try_parse_raw (line 869) | fn try_parse_raw<'de, T>(content: &mut &'de str)
  function line_number (line 891) | fn line_number(mmap: &[u8], byte_offset: usize) -> usize {
  function datum_time_from_string (line 915) | fn datum_time_from_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
  function new (line 1733) | pub fn new(config: &'a StatemapSVGConfig) -> Self {
  function output_defs (line 1739) | fn output_defs(&self, globals: &StatemapSVGGlobals)
  function title (line 1776) | fn title(&self, statemaps: &Vec<Statemap>) -> String
  function output (line 1834) | pub fn output(&self, statemaps: &Vec<Statemap>) ->
  function metadata (line 2130) | fn metadata(config: Option<&Config>, mut metadata: &str) -> Statemap {
  function minimal (line 2147) | fn minimal(config: Option<&Config>) -> Statemap {
  function data (line 2158) | fn data(config: Option<&Config>, data: Vec<&str>) -> Statemap {
  function bad_metadata (line 2172) | fn bad_metadata(mut metadata: &str, expected: &str) {
  function bad_datum (line 2189) | fn bad_datum(operand: Option<Statemap>, mut datum: &str, expected: &str) {
  function statemap_ingest (line 2217) | fn statemap_ingest(statemap: &mut Statemap, raw: &str)
  function bad_statemap (line 2234) | fn bad_statemap(raw: &str, expected: &str) {
  function good_statemap (line 2258) | fn good_statemap(config: &Config, raw: &str) -> Statemap {
  function good_minimal (line 2285) | fn good_minimal() {
  function bad_title_missing (line 2296) | fn bad_title_missing() {
  function bad_start_missing (line 2306) | fn bad_start_missing() {
  function bad_start_badval (line 2316) | fn bad_start_badval() {
  function bad_start_tooshort (line 2327) | fn bad_start_tooshort() {
  function bad_start_toolong (line 2338) | fn bad_start_toolong() {
  function bad_states_missing (line 2349) | fn bad_states_missing() {
  function bad_states_badmap (line 2357) | fn bad_states_badmap() {
  function bad_states_value_missing (line 2366) | fn bad_states_value_missing() {
  function bad_states_value_bad (line 2377) | fn bad_states_value_bad() {
  function bad_states_value_skipped1 (line 2388) | fn bad_states_value_skipped1() {
  function bad_states_value_toohigh (line 2400) | fn bad_states_value_toohigh() {
  function bad_states_value_duplicate (line 2412) | fn bad_states_value_duplicate() {
  function bad_line_basic (line 2424) | fn bad_line_basic() {
  function bad_line_whitespace (line 2429) | fn bad_line_whitespace() {
  function bad_line_newline (line 2434) | fn bad_line_newline() {
  function basic (line 2439) | fn basic() {
  function basic_datum (line 2466) | fn basic_datum() {
  function basic_description (line 2473) | fn basic_description() {
  function bad_datum_badtime (line 2484) | fn bad_datum_badtime() {
  function bad_datum_badtime_float (line 2491) | fn bad_datum_badtime_float() {
  function bad_datum_nostate (line 2498) | fn bad_datum_nostate() {
  function bad_datum_badstate (line 2505) | fn bad_datum_badstate() {
  function bad_datum_backwards (line 2512) | fn bad_datum_backwards() {
  function basic_data (line 2523) | fn basic_data() {
  function subsume (line 2537) | fn subsume() {
  function subsume_right (line 2555) | fn subsume_right() {
  function subsume_middle (line 2575) | fn subsume_middle() {
  function subsume_tagged (line 2593) | fn subsume_tagged() {
  function trim (line 2611) | fn trim() {
  function trim_insert (line 2638) | fn trim_insert() {
  function trim_multient (line 2667) | fn trim_multient() {
  function data_begin_time (line 2688) | fn data_begin_time() {
  function data_begin_time_later (line 2711) | fn data_begin_time_later() {
  function data_begin_end_time (line 2736) | fn data_begin_end_time() {
  function data_wrapped_time (line 2767) | fn data_wrapped_time() {
  function color_named (line 2793) | fn color_named() {
  function color_mix (line 2975) | fn color_mix() {
  function color_mix_linear (line 2995) | fn color_mix_linear() {
  function color_mix_nonlinear (line 3005) | fn color_mix_nonlinear() {
  function tag_basic (line 3015) | fn tag_basic() {
  function tag_redefined (line 3022) | fn tag_redefined() {
  function timebounds (line 3029) | fn timebounds() {
  function weight (line 3049) | fn weight() {
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (396K chars).
[
  {
    "path": ".gitmodules",
    "chars": 213,
    "preview": "[submodule \"deps/javascriptlint\"]\n\tpath = deps/javascriptlint\n\turl = git://github.com/davepacheco/javascriptlint.git\n[su"
  },
  {
    "path": "AUTHORS",
    "chars": 119,
    "preview": "# Statemap authors, ordered by first contribution\n\nBryan Cantrill <bryan@joyent.com>\nDavid Tolnay <dtolnay@gmail.com>\n\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 241,
    "preview": "[package]\nname = \"statemap\"\nversion = \"0.1.0\"\nauthors = [\"Bryan Cantrill <bryan@joyent.com>\"]\n\n[dependencies]\ngetopts = "
  },
  {
    "path": "LICENSE",
    "chars": 16725,
    "preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
  },
  {
    "path": "README.md",
    "chars": 10810,
    "preview": "# Statemap\n\nThis repository contains the software for rendering _statemaps_, a\nsoftware visualization in which time is o"
  },
  {
    "path": "contrib/cpu-statemap-tagged.d",
    "chars": 7577,
    "preview": "#!/usr/sbin/dtrace -Cs\n\n/*\n * Copyright 2018, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n#pra"
  },
  {
    "path": "contrib/cpu-statemap.d",
    "chars": 3279,
    "preview": "#!/usr/sbin/dtrace -Cs\n\n/*\n * Copyright 2018, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n#pra"
  },
  {
    "path": "contrib/io-statemap.d",
    "chars": 2401,
    "preview": "#!/usr/sbin/dtrace -Cs\n\n/*\n * Copyright 2018, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n\ninl"
  },
  {
    "path": "contrib/lx-cmd-statemap.d",
    "chars": 2214,
    "preview": "#!/usr/sbin/dtrace -Cs \n\n/*\n * Copyright 2017, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n\n#d"
  },
  {
    "path": "contrib/lx-statemap.d",
    "chars": 2366,
    "preview": "#!/usr/sbin/dtrace -Cs \n\n/*\n * Copyright 2017, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n\n#d"
  },
  {
    "path": "contrib/postgres-statemap.d",
    "chars": 3628,
    "preview": "#!/usr/sbin/dtrace -Cs \n\n/*\n * Copyright 2017, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n\n#d"
  },
  {
    "path": "contrib/postgres-zfs-statemap.d",
    "chars": 4225,
    "preview": "#!/usr/sbin/dtrace -Cs \n\n/*\n * Copyright 2018, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n\n#d"
  },
  {
    "path": "contrib/spa-sync-statemap.d",
    "chars": 3132,
    "preview": "#!/usr/sbin/dtrace -Cs \n\n/*\n * Copyright 2018, Joyent, Inc.\n */\n\n#pragma D option quiet\n\ntypedef enum {\n\tSTATE_ON_CPU = "
  },
  {
    "path": "contrib/vdev-statemap.d",
    "chars": 2901,
    "preview": "#!/usr/sbin/dtrace -Cs\n\n/*\n * Copyright 2018, Joyent, Inc.\n */\n\n#pragma D option quiet\n#pragma D option destructive\n\ntyp"
  },
  {
    "path": "src/icons/LICENSE",
    "chars": 1067,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "src/icons/README.md",
    "chars": 194,
    "preview": "\n# Statemap icons\n\nThe icons here are from the [svg-icon](https://leungwensen.github.io/svg-icon/)\nproject, and in parti"
  },
  {
    "path": "src/main.rs",
    "chars": 8783,
    "preview": "/*\n * Copyright 2018 Joyent, Inc.\n */ \n\n/*\n * We don't want to get away with not using values that we must use.\n */\n#![d"
  },
  {
    "path": "src/statemap-svg.css",
    "chars": 2616,
    "preview": "/*\n * Copyright 2018, Joyent, Inc.\n */\n\n.sansserif {\n\tfont-family: Verdana, Arial, Helvetica, sans-serif;\n}\n\n.button {\n "
  },
  {
    "path": "src/statemap-svg.defs",
    "chars": 428,
    "preview": "\n<!-- This file is pulled into the <defs> section !-->\n\n <marker id=\"startarrow\" markerWidth=\"10\" markerHeight=\"7\" \n    "
  },
  {
    "path": "src/statemap-svg.js",
    "chars": 33579,
    "preview": "/*\n * Copyright 2018, Joyent, Inc.\n */\n\n/*\n * This file is dropped into the generated SVG -- and if you're looking at\n *"
  },
  {
    "path": "src/statemap.rs",
    "chars": 99426,
    "preview": "/*\n * Copyright 2020 Joyent, Inc. and other contributors\n */ \n\nextern crate memmap;\nextern crate serde;\nextern crate ser"
  },
  {
    "path": "tst/tst.bad_line_basic.err",
    "chars": 47,
    "preview": "illegal datum on line 23: unrecognized payload\n"
  },
  {
    "path": "tst/tst.bad_line_basic.in",
    "chars": 1893,
    "preview": "{\n        \"start\": [ 1527898727, 987783377 ],\n        \"title\": \"PostgreSQL statemap on HA8SDNRD2, by process ID\",\n      "
  },
  {
    "path": "tst/tst.bad_line_newline.err",
    "chars": 47,
    "preview": "illegal datum on line 31: unrecognized payload\n"
  },
  {
    "path": "tst/tst.bad_line_newline.in",
    "chars": 1843,
    "preview": "{\n        \"start\": [ 1527898727, 987783377 ],\n        \"title\": \"PostgreSQL statemap on HA8SDNRD2, by process ID\",\n      "
  },
  {
    "path": "tst/tst.bad_line_whitespace.err",
    "chars": 47,
    "preview": "illegal datum on line 29: unrecognized payload\n"
  },
  {
    "path": "tst/tst.bad_line_whitespace.in",
    "chars": 1844,
    "preview": "{\n        \"start\": [ 1527898727, 987783377 ],\n        \"title\": \"PostgreSQL statemap on HA8SDNRD2, by process ID\",\n      "
  },
  {
    "path": "tst/tst.io.in",
    "chars": 4748,
    "preview": "{\n\t\"start\": [ 1515712765, 175059957 ],\n\t\"title\": \"Statemap for device I/O on HCFHM2TD2\",\n\t\"host\": \"HCFHM2TD2\",\n\t\"states\""
  },
  {
    "path": "tst/tst.tag_basic.in",
    "chars": 61880,
    "preview": "{\n\t\"start\": [ 1528482355, 856625707 ],\n\t\"title\": \"Statemap for CPU activity on HA8S7MRD2\",\n\t\"host\": \"HA8S7MRD2\",\n\t\"entit"
  },
  {
    "path": "tst/tst.tag_redefined.in",
    "chars": 72016,
    "preview": "{\n\t\"start\": [ 1528482355, 856625707 ],\n\t\"title\": \"Statemap for CPU activity on HA8S7MRD2\",\n\t\"host\": \"HA8S7MRD2\",\n\t\"entit"
  }
]

About this extraction

This page contains the full source code of the joyent/statemap GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (342.1 KB), approximately 116.7k tokens, and a symbol index with 123 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!