aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/utils/SkJSONWriter.h
diff options
context:
space:
mode:
authorGravatar Brian Osman <brianosman@google.com>2017-08-09 09:25:39 -0400
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2017-08-09 13:46:24 +0000
commit69fd008199989c5a5a96f992dcaa4089b63f490f (patch)
tree3bf276d326699010cb5e172d986e90e409ca7efd /src/utils/SkJSONWriter.h
parentbca3b8d8e0eaca73e25ed44fd20d4a6a0376e4ce (diff)
Added SkJSONWriter
This is a stand-alone helper class for writing properly structured JSON to an SkWStream. It currently solves two problems (although this CL only uses it in one context): 1) Performance. Writing out JSON this way is about 10x faster than using JSONCPP. For the large amounts of data generated by the tracing system, that's a big win. 2) Makes it easy to emit structured JSON from code that's not fully centralized. We'd like to spit out JSON that describes a GrContext, GrGpu, GrCaps, etc... Doing that with simple string manipulation is complex, and spreads this logic over all those functions. Using JSONCPP adds yet another (large) third party library dependency (that we only build into our own tools right now). This went through several revisions. I originally planned it as a stateful SkString wrapper, so the user could just build their JSON as a string. That's O(N^2), though, because SkString grows by a (small) constant amount. Even using a better growth strategy still means needing RAM for all the resulting text, which is usually pointless. This version has a constant memory cost, so writing huge amounts of JSON to disk (tracing a long DM run can emit 100's of MBs) doesn't stress resources. Bug: skia: Change-Id: Ia716524b246db0f97d332da60d2ce9903069e748 Reviewed-on: https://skia-review.googlesource.com/31204 Commit-Queue: Brian Osman <brianosman@google.com> Reviewed-by: Mike Klein <mtklein@chromium.org>
Diffstat (limited to 'src/utils/SkJSONWriter.h')
-rw-r--r--src/utils/SkJSONWriter.h329
1 files changed, 329 insertions, 0 deletions
diff --git a/src/utils/SkJSONWriter.h b/src/utils/SkJSONWriter.h
new file mode 100644
index 0000000000..9ac28ebc47
--- /dev/null
+++ b/src/utils/SkJSONWriter.h
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkJSONWriter_DEFINED
+#define SkJSONWriter_DEFINED
+
+#include "SkStream.h"
+
+#include <inttypes.h>
+
+/**
+ * Lightweight class for writing properly structured JSON data. No random-access, everything must
+ * be generated in-order. The resulting JSON is written directly to the SkWStream supplied at
+ * construction time. Output is buffered, so writing to disk (via an SkFILEWStream) is ideal.
+ *
+ * There is a basic state machine to ensure that JSON is structured correctly, and to allow for
+ * (optional) pretty formatting.
+ *
+ * This class adheres to the RFC-4627 usage of JSON (not ECMA-404). In other words, all JSON
+ * created with this class must have a top-level object or array. Free-floating values of other
+ * types are not considered valid.
+ *
+ * Note that all error checking is in the form of asserts - invalid usage in a non-debug build
+ * will simply produce invalid JSON.
+ */
+class SkJSONWriter : SkNoncopyable {
+public:
+ enum class Mode {
+ /**
+ * Output the minimal amount of text. No additional whitespace (including newlines) is
+ * generated. The resulting JSON is suitable for fast parsing and machine consumption.
+ */
+ kFast,
+
+ /**
+ * Output human-readable JSON, with indented objects and arrays, and one value per line.
+ * Slightly slower than kFast, and produces data that is somewhat larger.
+ */
+ kPretty
+ };
+
+ /**
+ * Construct a JSON writer that will serialize all the generated JSON to 'stream'.
+ */
+ SkJSONWriter(SkWStream* stream, Mode mode = Mode::kFast)
+ : fBlock(new char[kBlockSize])
+ , fWrite(fBlock)
+ , fBlockEnd(fBlock + kBlockSize)
+ , fStream(stream)
+ , fMode(mode)
+ , fState(State::kStart) {
+ fScopeStack.push_back(Scope::kNone);
+ }
+
+ ~SkJSONWriter() {
+ this->flush();
+ delete[] fBlock;
+ SkASSERT(fScopeStack.count() == 1);
+ }
+
+ /**
+ * Force all buffered output to be flushed to the underlying stream.
+ */
+ void flush() {
+ if (fWrite != fBlock) {
+ fStream->write(fBlock, fWrite - fBlock);
+ fWrite = fBlock;
+ }
+ }
+
+ /**
+ * Append the name (key) portion of an object member. Must be called between beginObject() and
+ * endObject(). If you have both the name and value of an object member, you can simply call
+ * the two argument versions of the other append functions.
+ */
+ void appendName(const char* name) {
+ if (!name) {
+ return;
+ }
+ SkASSERT(Scope::kObject == this->scope());
+ SkASSERT(State::kObjectBegin == fState || State::kObjectValue == fState);
+ if (State::kObjectValue == fState) {
+ this->write(",", 1);
+ }
+ this->newline();
+ this->write("\"", 1);
+ this->write(name, strlen(name));
+ this->write("\":", 2);
+ fState = State::kObjectName;
+ }
+
+ /**
+ * Adds a new object. A name must be supplied when called between beginObject() and
+ * endObject(). Calls to beginObject() must be balanced by corresponding calls to endObject().
+ */
+ void beginObject(const char* name = nullptr) {
+ this->appendName(name);
+ this->beginValue(true);
+ this->write("{", 1);
+ fScopeStack.push_back(Scope::kObject);
+ fState = State::kObjectBegin;
+ }
+
+ /**
+ * Ends an object that was previously started with beginObject().
+ */
+ void endObject() {
+ SkASSERT(Scope::kObject == this->scope());
+ SkASSERT(State::kObjectBegin == fState || State::kObjectValue == fState);
+ bool emptyObject = State::kObjectBegin == fState;
+ this->popScope();
+ if (!emptyObject) {
+ this->newline();
+ }
+ this->write("}", 1);
+ }
+
+ /**
+ * Adds a new array. A name must be supplied when called between beginObject() and
+ * endObject(). Calls to beginArray() must be balanced by corresponding calls to endArray().
+ */
+ void beginArray(const char* name = nullptr) {
+ this->appendName(name);
+ this->beginValue(true);
+ this->write("[", 1);
+ fScopeStack.push_back(Scope::kArray);
+ fState = State::kArrayBegin;
+ }
+
+ /**
+ * Ends an array that was previous started with beginArray().
+ */
+ void endArray() {
+ SkASSERT(Scope::kArray == this->scope());
+ SkASSERT(State::kArrayBegin == fState || State::kArrayValue == fState);
+ bool emptyArray = State::kArrayBegin == fState;
+ this->popScope();
+ if (!emptyArray) {
+ this->newline();
+ }
+ this->write("]", 1);
+ }
+
+ /**
+ * Functions for adding values of various types. The single argument versions add un-named
+ * values, so must be called either
+ * - Between beginArray() and endArray() -or-
+ * - Between beginObject() and endObject(), after calling appendName()
+ */
+ void appendString(const char* value) {
+ this->beginValue();
+ this->write("\"", 1);
+ if (value) {
+ while (*value) {
+ switch (*value) {
+ case '"': this->write("\\\"", 2); break;
+ case '\\': this->write("\\\\", 2); break;
+ case '\b': this->write("\\b", 2); break;
+ case '\f': this->write("\\f", 2); break;
+ case '\n': this->write("\\n", 2); break;
+ case '\r': this->write("\\r", 2); break;
+ case '\t': this->write("\\t", 2); break;
+ default: this->write(value, 1); break;
+ }
+ value++;
+ }
+ }
+ this->write("\"", 1);
+ }
+
+ void appendPointer(const void* value) { this->beginValue(); this->appendf("\"%p\"", value); }
+ void appendBool(bool value) {
+ this->beginValue();
+ if (value) {
+ this->write("true", 4);
+ } else {
+ this->write("false", 5);
+ }
+ }
+ void appendS32(int32_t value) { this->beginValue(); this->appendf("%d", value); }
+ void appendS64(int64_t value) { this->beginValue(); this->appendf("%" PRId64, value); }
+ void appendU32(uint32_t value) { this->beginValue(); this->appendf("%u", value); }
+ void appendU64(uint64_t value) { this->beginValue(); this->appendf("%" PRIu64, value); }
+ void appendFloat(float value) { this->beginValue(); this->appendf("%f", value);; }
+ void appendDouble(double value) { this->beginValue(); this->appendf("%f", value); }
+ void appendHexU32(uint32_t value) { this->beginValue(); this->appendf("\"0x%x\"", value); }
+ void appendHexU64(uint64_t value) { this->beginValue(); this->appendf("\"0x%" PRIx64 "\"", value); }
+
+#define DEFINE_NAMED_APPEND(function, type) \
+ void function(const char* name, type value) { this->appendName(name); this->function(value); }
+
+ /**
+ * Functions for adding named values of various types. These add a name field, so must be
+ * called between beginObject() and endObject().
+ */
+ DEFINE_NAMED_APPEND(appendString, const char *)
+ DEFINE_NAMED_APPEND(appendPointer, const void *)
+ DEFINE_NAMED_APPEND(appendBool, bool)
+ DEFINE_NAMED_APPEND(appendS32, int32_t)
+ DEFINE_NAMED_APPEND(appendS64, int64_t)
+ DEFINE_NAMED_APPEND(appendU32, uint32_t)
+ DEFINE_NAMED_APPEND(appendU64, uint64_t)
+ DEFINE_NAMED_APPEND(appendFloat, float)
+ DEFINE_NAMED_APPEND(appendDouble, double)
+ DEFINE_NAMED_APPEND(appendHexU32, uint32_t)
+ DEFINE_NAMED_APPEND(appendHexU64, uint64_t)
+
+#undef DEFINE_NAMED_APPEND
+
+private:
+ enum {
+ // Using a 32k scratch block gives big performance wins, but we diminishing returns going
+ // any larger. Even with a 1MB block, time to write a large (~300 MB) JSON file only drops
+ // another ~10%.
+ kBlockSize = 32 * 1024,
+ };
+
+ enum class Scope {
+ kNone,
+ kObject,
+ kArray
+ };
+
+ enum class State {
+ kStart,
+ kEnd,
+ kObjectBegin,
+ kObjectName,
+ kObjectValue,
+ kArrayBegin,
+ kArrayValue,
+ };
+
+ void appendf(const char* fmt, ...) {
+ const int kBufferSize = 1024;
+ char buffer[kBufferSize];
+ va_list argp;
+ va_start(argp, fmt);
+#ifdef SK_BUILD_FOR_WIN
+ int length = _vsnprintf_s(buffer, kBufferSize, _TRUNCATE, fmt, argp);
+#else
+ int length = vsnprintf(buffer, kBufferSize, fmt, argp);
+#endif
+ SkASSERT(length >= 0 && length < kBufferSize);
+ va_end(argp);
+ this->write(buffer, length);
+ }
+
+ void beginValue(bool structure = false) {
+ SkASSERT(State::kObjectName == fState ||
+ State::kArrayBegin == fState ||
+ State::kArrayValue == fState ||
+ (structure && State::kStart == fState));
+ if (State::kArrayValue == fState) {
+ this->write(",", 1);
+ }
+ if (Scope::kArray == this->scope()) {
+ this->newline();
+ } else if (Scope::kObject == this->scope() && Mode::kPretty == fMode) {
+ this->write(" ", 1);
+ }
+ // We haven't added the value yet, but all (non-structure) callers emit something
+ // immediately, so transition state, to simplify the calling code.
+ if (!structure) {
+ fState = Scope::kArray == this->scope() ? State::kArrayValue : State::kObjectValue;
+ }
+ }
+
+ void newline() {
+ if (Mode::kPretty == fMode) {
+ this->write("\n", 1);
+ for (int i = 0; i < fScopeStack.count() - 1; ++i) {
+ this->write(" ", 3);
+ }
+ }
+ }
+
+ void write(const char* buf, size_t length) {
+ if (static_cast<size_t>(fBlockEnd - fWrite) < length) {
+ // Don't worry about splitting writes that overflow our block.
+ this->flush();
+ }
+ if (length > kBlockSize) {
+ // Send particularly large writes straight through to the stream (unbuffered).
+ fStream->write(buf, length);
+ } else {
+ memcpy(fWrite, buf, length);
+ fWrite += length;
+ }
+ }
+
+ Scope scope() const {
+ SkASSERT(!fScopeStack.empty());
+ return fScopeStack.back();
+ }
+
+ void popScope() {
+ fScopeStack.pop_back();
+ switch (this->scope()) {
+ case Scope::kNone:
+ fState = State::kEnd;
+ break;
+ case Scope::kObject:
+ fState = State::kObjectValue;
+ break;
+ case Scope::kArray:
+ fState = State::kArrayValue;
+ break;
+ default:
+ SkDEBUGFAIL("Invalid scope");
+ break;
+ }
+ }
+
+ char* fBlock;
+ char* fWrite;
+ char* fBlockEnd;
+
+ SkWStream* fStream;
+ Mode fMode;
+ State fState;
+ SkSTArray<16, Scope, true> fScopeStack;
+};
+
+#endif