aboutsummaryrefslogtreecommitdiffhomepage
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
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>
-rw-r--r--gn/utils.gni1
-rw-r--r--src/utils/SkJSONWriter.h329
-rw-r--r--tools/trace/SkChromeTracingTracer.cpp134
-rw-r--r--tools/trace/SkChromeTracingTracer.h5
4 files changed, 405 insertions, 64 deletions
diff --git a/gn/utils.gni b/gn/utils.gni
index 14bec37def..faab5d9e1b 100644
--- a/gn/utils.gni
+++ b/gn/utils.gni
@@ -45,6 +45,7 @@ skia_utils_sources = [
"$_src/utils/SkInsetConvexPolygon.cpp",
"$_src/utils/SkInsetConvexPolygon.h",
"$_src/utils/SkInterpolator.cpp",
+ "$_src/utils/SkJSONWriter.h",
"$_src/utils/SkMatrix22.cpp",
"$_src/utils/SkMatrix22.h",
"$_src/utils/SkMultiPictureDocument.cpp",
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
diff --git a/tools/trace/SkChromeTracingTracer.cpp b/tools/trace/SkChromeTracingTracer.cpp
index fd9e8d1c3d..ced4f74fa9 100644
--- a/tools/trace/SkChromeTracingTracer.cpp
+++ b/tools/trace/SkChromeTracingTracer.cpp
@@ -6,6 +6,7 @@
*/
#include "SkChromeTracingTracer.h"
+#include "SkJSONWriter.h"
#include "SkThreadID.h"
#include "SkTraceEvent.h"
#include "SkOSFile.h"
@@ -86,31 +87,38 @@ void SkChromeTracingTracer::updateTraceEventDuration(const uint8_t* categoryEnab
traceEvent->fClockEnd = std::chrono::high_resolution_clock::now().time_since_epoch().count();
}
-static Json::Value trace_value_to_json(uint64_t argValue, uint8_t argType) {
+static void trace_value_to_json(SkJSONWriter* writer, uint64_t argValue, uint8_t argType) {
skia::tracing_internals::TraceValueUnion value;
value.as_uint = argValue;
switch (argType) {
case TRACE_VALUE_TYPE_BOOL:
- return Json::Value(value.as_bool);
+ writer->appendBool(value.as_bool);
+ break;
case TRACE_VALUE_TYPE_UINT:
- return Json::Value(static_cast<Json::UInt64>(value.as_uint));
+ writer->appendU64(value.as_uint);
+ break;
case TRACE_VALUE_TYPE_INT:
- return Json::Value(static_cast<Json::Int64>(value.as_uint));
+ writer->appendS64(value.as_int);
+ break;
case TRACE_VALUE_TYPE_DOUBLE:
- return Json::Value(value.as_double);
+ writer->appendDouble(value.as_double);
+ break;
case TRACE_VALUE_TYPE_POINTER:
- return Json::Value(SkStringPrintf("%p", value.as_pointer).c_str());
+ writer->appendPointer(value.as_pointer);
+ break;
case TRACE_VALUE_TYPE_STRING:
case TRACE_VALUE_TYPE_COPY_STRING:
- return Json::Value(value.as_string);
+ writer->appendString(value.as_string);
+ break;
default:
- return Json::Value("<unknown type>");
+ writer->appendString("<unknown type>");
+ break;
}
}
-Json::Value SkChromeTracingTracer::traceEventToJson(const TraceEvent& traceEvent,
- BaseTypeResolver* baseTypeResolver) {
+void SkChromeTracingTracer::traceEventToJson(SkJSONWriter* writer, const TraceEvent& traceEvent,
+ BaseTypeResolver* baseTypeResolver) {
// We track the original (creation time) "name" of each currently live object, so we can
// automatically insert "base_name" fields in object snapshot events.
if (TRACE_EVENT_PHASE_CREATE_OBJECT == traceEvent.fPhase) {
@@ -121,89 +129,91 @@ Json::Value SkChromeTracingTracer::traceEventToJson(const TraceEvent& traceEvent
baseTypeResolver->remove(traceEvent.fID);
}
- Json::Value json;
+ writer->beginObject();
+
char phaseString[2] = { traceEvent.fPhase, 0 };
- json["ph"] = phaseString;
- json["name"] = traceEvent.fName;
- json["cat"] = traceEvent.fCategory;
+ writer->appendString("ph", phaseString);
+ writer->appendString("name", traceEvent.fName);
+ writer->appendString("cat", traceEvent.fCategory);
if (0 != traceEvent.fID) {
// IDs are (almost) always pointers
- json["id"] = SkStringPrintf("%p", traceEvent.fID).c_str();
+ writer->appendPointer("id", reinterpret_cast<void*>(traceEvent.fID));
}
// Convert nanoseconds to microseconds (standard time unit for tracing JSON files)
- json["ts"] = static_cast<double>(traceEvent.fClockBegin) * 1E-3;
+ writer->appendDouble("ts", static_cast<double>(traceEvent.fClockBegin) * 1E-3);
if (0 != traceEvent.fClockEnd) {
- json["dur"] = static_cast<double>(traceEvent.fClockEnd - traceEvent.fClockBegin) * 1E-3;
+ double dur = static_cast<double>(traceEvent.fClockEnd - traceEvent.fClockBegin) * 1E-3;
+ writer->appendDouble("dur", dur);
}
- json["tid"] = static_cast<Json::Int64>(traceEvent.fThreadID);
+ writer->appendS64("tid", traceEvent.fThreadID);
// Trace events *must* include a process ID, but for internal tools this isn't particularly
// important (and certainly not worth adding a cross-platform API to get it).
- json["pid"] = 0;
+ writer->appendS32("pid", 0);
if (traceEvent.fNumArgs) {
- Json::Value args;
+ writer->beginObject("args");
+
+ bool addedSnapshot = false;
+ if (TRACE_EVENT_PHASE_SNAPSHOT_OBJECT == traceEvent.fPhase &&
+ baseTypeResolver->find(traceEvent.fID) &&
+ 0 != strcmp(*baseTypeResolver->find(traceEvent.fID), traceEvent.fName)) {
+ // Special handling for snapshots where the name differs from creation.
+ writer->beginObject("snapshot");
+ writer->appendString("base_type", *baseTypeResolver->find(traceEvent.fID));
+ addedSnapshot = true;
+ }
+
for (int i = 0; i < traceEvent.fNumArgs; ++i) {
- Json::Value argValue = trace_value_to_json(traceEvent.fArgValues[i],
- traceEvent.fArgTypes[i]);
+ // TODO: Skip '#'
+ writer->appendName(traceEvent.fArgNames[i]);
+
if (traceEvent.fArgNames[i] && '#' == traceEvent.fArgNames[i][0]) {
- // Interpret #foo as an ID reference
- Json::Value idRef;
- idRef["id_ref"] = argValue;
- args[traceEvent.fArgNames[i] + 1] = idRef;
+ writer->beginObject();
+ writer->appendName("id_ref");
+ trace_value_to_json(writer, traceEvent.fArgValues[i], traceEvent.fArgTypes[i]);
+ writer->endObject();
} else {
- args[traceEvent.fArgNames[i]] = argValue;
+ trace_value_to_json(writer, traceEvent.fArgValues[i], traceEvent.fArgTypes[i]);
}
}
- if (TRACE_EVENT_PHASE_SNAPSHOT_OBJECT == traceEvent.fPhase &&
- baseTypeResolver->find(traceEvent.fID) &&
- 0 != strcmp(*baseTypeResolver->find(traceEvent.fID), traceEvent.fName)) {
- // Special handling for snapshots where the name differs from creation.
- // We start with args = { "snapshot": "Object info" }
-
- // Inject base_type. args = { "snapshot": "Object Info", "base_type": "BaseFoo" }
- args["base_type"] = *baseTypeResolver->find(traceEvent.fID);
-
- // Wrap this up in a new dict, again keyed by "snapshot". The inner "snapshot" is now
- // arbitrary, but we don't have a better name (and the outer key *must* be snapshot).
- // snapshot = { "snapshot": { "snapshot": "Object Info", "base_type": "BaseFoo" } }
- Json::Value snapshot;
- snapshot["snapshot"] = args;
-
- // Now insert that whole thing as the event's args.
- // { "name": "DerivedFoo", "id": "0x12345678, ...
- // "args": { "snapshot": { "snapshot": "Object Info", "base_type": "BaseFoo" } }
- // }, ...
- json["args"] = snapshot;
- } else {
- json["args"] = args;
+
+ if (addedSnapshot) {
+ writer->endObject();
}
+
+ writer->endObject();
}
- return json;
+
+ writer->endObject();
}
void SkChromeTracingTracer::flush() {
SkAutoMutexAcquire lock(fMutex);
- Json::Value root(Json::arrayValue);
+ SkString dirname = SkOSPath::Dirname(fFilename.c_str());
+ if (!dirname.isEmpty() && !sk_exists(dirname.c_str(), kWrite_SkFILE_Flag)) {
+ if (!sk_mkdir(dirname.c_str())) {
+ SkDebugf("Failed to create directory.");
+ }
+ }
+
+ SkFILEWStream fileStream(fFilename.c_str());
+ SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kFast);
+ writer.beginArray();
+
BaseTypeResolver baseTypeResolver;
for (int i = 0; i < fBlocks.count(); ++i) {
for (int j = 0; j < kEventsPerBlock; ++j) {
- root.append(this->traceEventToJson(fBlocks[i][j], &baseTypeResolver));
+ this->traceEventToJson(&writer, fBlocks[i][j], &baseTypeResolver);
}
}
for (int i = 0; i < fEventsInCurBlock; ++i) {
- root.append(this->traceEventToJson(fCurBlock[i], &baseTypeResolver));
+ this->traceEventToJson(&writer, fCurBlock[i], &baseTypeResolver);
}
- SkString dirname = SkOSPath::Dirname(fFilename.c_str());
- if (!dirname.isEmpty() && !sk_exists(dirname.c_str(), kWrite_SkFILE_Flag)) {
- if (!sk_mkdir(dirname.c_str())) {
- SkDebugf("Failed to create directory.");
- }
- }
- SkFILEWStream stream(fFilename.c_str());
- stream.writeText(Json::FastWriter().write(root).c_str());
- stream.flush();
+ writer.endArray();
+ writer.flush();
+ fileStream.flush();
}
diff --git a/tools/trace/SkChromeTracingTracer.h b/tools/trace/SkChromeTracingTracer.h
index 987031641b..c6411bb69f 100644
--- a/tools/trace/SkChromeTracingTracer.h
+++ b/tools/trace/SkChromeTracingTracer.h
@@ -10,11 +10,12 @@
#include "SkEventTracer.h"
#include "SkEventTracingPriv.h"
-#include "SkJSONCPP.h"
#include "SkSpinlock.h"
#include "SkString.h"
#include "SkTHash.h"
+class SkJSONWriter;
+
/**
* A SkEventTracer implementation that logs events to JSON for viewing with chrome://tracing.
*/
@@ -80,7 +81,7 @@ private:
typedef SkTHashMap<uint64_t, const char*> BaseTypeResolver;
TraceEvent* appendEvent(const TraceEvent&);
- Json::Value traceEventToJson(const TraceEvent&, BaseTypeResolver* baseTypeResolver);
+ void traceEventToJson(SkJSONWriter*, const TraceEvent&, BaseTypeResolver* baseTypeResolver);
SkString fFilename;
SkSpinlock fMutex;