/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #ifndef HERMES_SYNTHTRACE_H #define HERMES_SYNTHTRACE_H #ifdef HERMESVM_API_TRACE #include "hermes/Public/RuntimeConfig.h" #include "hermes/Support/JSONEmitter.h" #include "hermes/Support/StringSetVector.h" #include "hermes/VM/HermesValue.h" #include "hermes/VM/MockedEnvironment.h" #include "hermes/VM/Operations.h" #include #include #include #include #include namespace llvm { // Forward declaration to avoid including llvm headers. class raw_ostream; } // namespace llvm namespace facebook { namespace hermes { namespace tracing { /// A SynthTrace is a list of events that occur in a run of a JS file by a /// runtime that uses JSI. /// It can be serialized into JSON and written to a llvm::raw_ostream. class SynthTrace { public: using ObjectID = uint64_t; /// A tagged union representing different types available in the trace. /// HermesValue doesn't have to be used, but it is an efficient way /// of encoding a type tag and a value. std::variant is another option. /// NOTE: Since HermesValue can only store 64-bit values, strings need to be /// changed into a table index. using TraceValue = ::hermes::vm::HermesValue; /// A TimePoint is a time when some event occurred. using TimePoint = std::chrono::steady_clock::time_point; using TimeSinceStart = std::chrono::milliseconds; static constexpr size_t kHashNumBytes = 20; /// RecordType is a tag used to differentiate which type of record it is. /// There should be a unique tag for each record type. enum class RecordType { BeginExecJS, EndExecJS, Marker, CreateObject, CreateHostObject, CreateHostFunction, GetProperty, SetProperty, HasProperty, GetPropertyNames, CreateArray, ArrayRead, ArrayWrite, CallFromNative, ConstructFromNative, ReturnFromNative, ReturnToNative, CallToNative, GetPropertyNative, GetPropertyNativeReturn, SetPropertyNative, SetPropertyNativeReturn, GetNativePropertyNames, GetNativePropertyNamesReturn, }; /// A Record is one element of a trace. struct Record { /// The time at which this event occurred with respect to the start of /// execution. /// NOTE: This is not compared in the \c operator= in order for tests to /// pass. const TimeSinceStart time_; explicit Record() = delete; explicit Record(TimeSinceStart time) : time_(time) {} virtual ~Record() = default; /// Write out a serialization of this Record. /// \param json An emitter connected to an ostream which will write out /// JSON. void toJSON(::hermes::JSONEmitter &json, const SynthTrace &trace) const; virtual RecordType getType() const = 0; /// \return A list of object ids that are defined by this record. /// Defined means that the record would produce that object as a locally /// accessible value if it were executed. virtual std::vector defs() const { return {}; } /// \return A list of object ids that are used by this record. /// Used means that the record would use that object as a value if it were /// executed. /// If a record uses an object, then some preceding record (either in the /// same function invocation, or somewhere globally) must provide a /// definition. virtual std::vector uses() const { return {}; } protected: /// Compare records for equality. Derived classes should override this, call /// their parent, and mark any public versions as "final". virtual bool operator==(const Record &that) const { return getType() == that.getType(); } /// Emit JSON fields into \p os, excluding the closing curly brace. /// NOTE: This is overridable, and non-abstract children should call the /// parent. virtual void toJSONInternal( ::hermes::JSONEmitter &json, const SynthTrace &trace) const; }; /// If \p traceStream is non-null, the trace will be written to that /// stream. Otherwise, no trace is written. explicit SynthTrace( ObjectID globalObjID, const ::hermes::vm::RuntimeConfig &conf, std::unique_ptr traceStream = nullptr); template void emplace_back(Args &&... args) { records_.emplace_back(new T(std::forward(args)...)); flushRecordsIfNecessary(); } const std::vector> &records() const { return records_; } ObjectID globalObjID() const { return globalObjID_; } /// Given a trace value, turn it into its typed string. std::string encode(TraceValue value) const; /// Encode an undefined JS value for the trace. static TraceValue encodeUndefined(); /// Encode a null JS value for the trace. static TraceValue encodeNull(); /// Encode a boolean JS value for the trace. static TraceValue encodeBool(bool value); /// Encodes a numeric value for the trace. static TraceValue encodeNumber(double value); /// Encodes an object for the trace as a unique id. static TraceValue encodeObject(ObjectID objID); /// Encodes a string for the trace. Adds it to the string table if it hasn't /// been seen before. TraceValue encodeString(const std::string &value); /// Decodes a string into a trace value. It can add to the string table. TraceValue decode(const std::string &); /// Extracts an object ID from a trace value. /// \pre The value must be an object. static ObjectID decodeObject(TraceValue value); /// Extracts a string from a trace value. /// \pre The value must be a string. const std::string &decodeString(TraceValue value) const; static bool equal(TraceValue x, TraceValue y) { // We are encoding random numbers into strings, and can't use the library // functions. // For now, ignore differences that result from NaN, +0/-0. return x.getRaw() == y.getRaw(); } /// The version of the Synth Benchmark constexpr static uint32_t synthVersion() { return 2; } static const char *nameFromReleaseUnused(::hermes::vm::ReleaseUnused ru); static ::hermes::vm::ReleaseUnused releaseUnusedFromName(const char *name); private: llvm::raw_ostream &os() const { return (*traceStream_); } /// If we're tracing to a file, and the number of accumulated /// records has reached the limit kTraceRecordsToFlush, below, /// flush the records to the file, and reset the accumulated records /// to be empty. void flushRecordsIfNecessary(); /// Assumes we're tracing to a file; flush accumulated records to /// the file, and reset the accumulated records to be empty. void flushRecords(); static constexpr unsigned kTraceRecordsToFlush = 100; /// If we're tracing to a file, pointer to a stream onto /// traceFilename_. Null otherwise. std::unique_ptr traceStream_; /// If we're tracing to a file, pointer to a JSONEmitter writting /// into *traceStream_. Null otherwise. std::unique_ptr<::hermes::JSONEmitter> json_; /// The records currently being accumulated in the trace. If we are /// tracing to a file, these will be only the records not yet /// written to the file. std::vector> records_; /// The id of the global object. const ObjectID globalObjID_; /// A table of strings to avoid repeated strings taking up memory. Similar to /// the IdentifierTable, except it doesn't need to be collected (it stores /// strings forever). /// Strings are stored in the trace objects as an index into this table. ::hermes::StringSetVector stringTable_; public: /// @name Record classes /// @{ /// A MarkerRecord is an event that simply records an interesting event that /// is not necessarily meaningful to the interpreter. It comes with a tag that /// says what type of marker it was. struct MarkerRecord : public Record { static constexpr RecordType type{RecordType::Marker}; const std::string tag_; explicit MarkerRecord(TimeSinceStart time, const std::string &tag) : Record(time), tag_(tag) {} RecordType getType() const override { return type; } protected: void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; bool operator==(const Record &that) const override; }; /// A BeginExecJSRecord is an event where execution begins of JS source /// code. This is not necessarily the first record, since native code can /// inject values into the VM before any source code is run. struct BeginExecJSRecord final : public Record { static constexpr RecordType type{RecordType::BeginExecJS}; explicit BeginExecJSRecord( TimeSinceStart time, std::string sourceURL, ::hermes::SHA1 sourceHash) : Record(time), sourceURL_(std::move(sourceURL)), sourceHash_(std::move(sourceHash)) {} RecordType getType() const override { return type; } const std::string &sourceURL() const { return sourceURL_; } const ::hermes::SHA1 &sourceHash() const { return sourceHash_; } private: void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; /// The URL providing the source file mapping for the file being executed. /// Can be empty. std::string sourceURL_; /// A hash of the source that was executed. The source hash must match up /// when the file is replayed. /// The hash is optional, and will be all zeros if not provided. ::hermes::SHA1 sourceHash_; }; struct ReturnMixin { const TraceValue retVal_; explicit ReturnMixin(TraceValue value) : retVal_(value) {} void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const; bool operator==(const ReturnMixin &that) const; }; /// A EndExecJSRecord is an event where execution of JS source code stops. /// This does not mean that the source code will never be entered again, just /// that it has an entered a phase where it is waiting for native code to call /// into the JS. This event is not guaranteed to be the last event, for the /// aforementioned reason. The logged retVal is the result of the evaluation /// ("undefined" in the majority of cases). struct EndExecJSRecord final : public MarkerRecord, public ReturnMixin { static constexpr RecordType type{RecordType::EndExecJS}; EndExecJSRecord(TimeSinceStart time, TraceValue retVal) : MarkerRecord(time, "end_global_code"), ReturnMixin(retVal) {} RecordType getType() const override { return type; } bool operator==(const Record &that) const final; virtual void toJSONInternal( ::hermes::JSONEmitter &json, const SynthTrace &trace) const final; std::vector defs() const override { auto defs = MarkerRecord::defs(); if (retVal_.isObject()) { defs.push_back(decodeObject(retVal_)); } return defs; } }; /// A CreateObjectRecord is an event where an empty object is created by the /// native code. struct CreateObjectRecord : public Record { static constexpr RecordType type{RecordType::CreateObject}; const ObjectID objID_; explicit CreateObjectRecord(TimeSinceStart time, ObjectID objID) : Record(time), objID_(objID) {} bool operator==(const Record &that) const override; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; RecordType getType() const override { return type; } std::vector defs() const override { return {objID_}; } std::vector uses() const override { return {}; } }; struct CreateHostObjectRecord final : public CreateObjectRecord { static constexpr RecordType type{RecordType::CreateHostObject}; using CreateObjectRecord::CreateObjectRecord; RecordType getType() const override { return type; } }; struct CreateHostFunctionRecord final : public CreateObjectRecord { static constexpr RecordType type{RecordType::CreateHostFunction}; const std::string functionName_; const unsigned paramCount_; CreateHostFunctionRecord( TimeSinceStart time, ObjectID objID, std::string functionName, unsigned paramCount) : CreateObjectRecord(time, objID), functionName_(std::move(functionName)), paramCount_(paramCount) {} bool operator==(const Record &that) const override; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; RecordType getType() const override { return type; } }; struct GetOrSetPropertyRecord : public Record { const ObjectID objID_; const std::string propName_; const TraceValue value_; explicit GetOrSetPropertyRecord( TimeSinceStart time, ObjectID objID, const std::string &propName, TraceValue value) : Record(time), objID_(objID), propName_(propName), value_(value) {} bool operator==(const Record &that) const final; std::vector uses() const override { return {objID_}; } void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; }; /// A GetPropertyRecord is an event where native code accesses the property /// of a JS object. struct GetPropertyRecord : public GetOrSetPropertyRecord { static constexpr RecordType type{RecordType::GetProperty}; using GetOrSetPropertyRecord::GetOrSetPropertyRecord; RecordType getType() const override { return type; } std::vector defs() const override { auto defs = GetOrSetPropertyRecord::defs(); if (value_.isObject()) { defs.push_back(decodeObject(value_)); } return defs; } }; /// A SetPropertyRecord is an event where native code writes to the property /// of a JS object. struct SetPropertyRecord : public GetOrSetPropertyRecord { static constexpr RecordType type{RecordType::SetProperty}; using GetOrSetPropertyRecord::GetOrSetPropertyRecord; RecordType getType() const override { return type; } std::vector uses() const override { auto uses = GetOrSetPropertyRecord::uses(); if (value_.isObject()) { uses.push_back(decodeObject(value_)); } return uses; } }; /// A HasPropertyRecord is an event where native code queries whether a /// property exists on an object. (We don't care about the result because /// it cannot influence the trace.) struct HasPropertyRecord final : public Record { static constexpr RecordType type{RecordType::HasProperty}; const ObjectID objID_; const std::string propName_; explicit HasPropertyRecord( TimeSinceStart time, ObjectID objID, const std::string &propName) : Record(time), objID_(objID), propName_(propName) {} bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; RecordType getType() const override { return type; } std::vector uses() const override { return {objID_}; } }; struct GetPropertyNamesRecord final : public Record { static constexpr RecordType type{RecordType::GetPropertyNames}; const ObjectID objID_; // Since getPropertyNames always returns an array, this can be an object id // rather than a TraceValue. const ObjectID propNamesID_; explicit GetPropertyNamesRecord( TimeSinceStart time, ObjectID objID, ObjectID propNamesID) : Record(time), objID_(objID), propNamesID_(propNamesID) {} bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; RecordType getType() const override { return type; } std::vector defs() const override { return {propNamesID_}; } std::vector uses() const override { return {objID_}; } }; /// A CreateArrayRecord is an event where a new array is created of a specific /// length. struct CreateArrayRecord final : public Record { static constexpr RecordType type{RecordType::CreateArray}; const ObjectID objID_; const size_t length_; explicit CreateArrayRecord( TimeSinceStart time, ObjectID objID, size_t length) : Record(time), objID_(objID), length_(length) {} bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; RecordType getType() const override { return type; } std::vector defs() const override { return {objID_}; } }; struct ArrayReadOrWriteRecord : public Record { const ObjectID objID_; const size_t index_; const TraceValue value_; explicit ArrayReadOrWriteRecord( TimeSinceStart time, ObjectID objID, size_t index, TraceValue value) : Record(time), objID_(objID), index_(index), value_(value) {} bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; std::vector uses() const override { return {objID_}; } }; /// An ArrayReadRecord is an event where a value was read from an index /// of an array. /// It is modeled separately from GetProperty because it is more efficient to /// read from a numeric index on an array than a string. struct ArrayReadRecord final : public ArrayReadOrWriteRecord { static constexpr RecordType type{RecordType::ArrayRead}; using ArrayReadOrWriteRecord::ArrayReadOrWriteRecord; RecordType getType() const override { return type; } std::vector defs() const override { auto defs = ArrayReadOrWriteRecord::defs(); if (value_.isObject()) { defs.push_back(decodeObject(value_)); } return defs; } }; /// An ArrayWriteRecord is an event where a value was written into an index /// of an array. struct ArrayWriteRecord final : public ArrayReadOrWriteRecord { static constexpr RecordType type{RecordType::ArrayWrite}; using ArrayReadOrWriteRecord::ArrayReadOrWriteRecord; RecordType getType() const override { return type; } std::vector uses() const override { auto uses = ArrayReadOrWriteRecord::uses(); if (value_.isObject()) { uses.push_back(decodeObject(value_)); } return uses; } }; struct CallRecord : public Record { /// The functionID_ is the id of the function JS object that is called from /// JS. const ObjectID functionID_; const TraceValue thisArg_; /// The arguments given to a call (excluding the this parameter), /// already JSON stringified. const std::vector args_; explicit CallRecord( TimeSinceStart time, ObjectID functionID, TraceValue thisArg, const std::vector &args) : Record(time), functionID_(functionID), thisArg_(thisArg), args_(args) {} bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; std::vector uses() const override { // The function is used regardless of direction. return {functionID_}; } protected: std::vector getArgObjects() const { std::vector objs; if (thisArg_.isObject()) { objs.push_back(decodeObject(thisArg_)); } for (const auto &arg : args_) { if (arg.isObject()) { objs.push_back(decodeObject(arg)); } } return objs; } }; /// A CallFromNativeRecord is an event where native code calls into a JS /// function. struct CallFromNativeRecord : public CallRecord { static constexpr RecordType type{RecordType::CallFromNative}; using CallRecord::CallRecord; RecordType getType() const override { return type; } std::vector uses() const override { auto uses = CallRecord::uses(); auto objs = CallRecord::getArgObjects(); uses.insert(uses.end(), objs.begin(), objs.end()); return uses; } }; /// A ConstructFromNativeRecord is the same as \c CallFromNativeRecord, except /// the function is called with the new operator. struct ConstructFromNativeRecord final : public CallFromNativeRecord { static constexpr RecordType type{RecordType::ConstructFromNative}; using CallFromNativeRecord::CallFromNativeRecord; RecordType getType() const override { return type; } }; /// A ReturnFromNativeRecord is an event where a native function returns to a /// JS caller. /// It pairs with \c CallToNativeRecord. struct ReturnFromNativeRecord final : public Record, public ReturnMixin { static constexpr RecordType type{RecordType::ReturnFromNative}; ReturnFromNativeRecord(TimeSinceStart time, TraceValue retVal) : Record(time), ReturnMixin(retVal) {} RecordType getType() const override { return type; } std::vector uses() const override { auto uses = Record::uses(); if (retVal_.isObject()) { uses.push_back(decodeObject(retVal_)); } return uses; } bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; }; /// A ReturnToNativeRecord is an event where a JS function returns to a native /// caller. /// It pairs with \c CallFromNativeRecord. struct ReturnToNativeRecord final : public Record, public ReturnMixin { static constexpr RecordType type{RecordType::ReturnToNative}; ReturnToNativeRecord(TimeSinceStart time, TraceValue retVal) : Record(time), ReturnMixin(retVal) {} RecordType getType() const override { return type; } std::vector defs() const override { auto defs = Record::defs(); if (retVal_.isObject()) { defs.push_back(decodeObject(retVal_)); } return defs; } bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; }; /// A CallToNativeRecord is an event where JS code calls into a natively /// defined function. struct CallToNativeRecord final : public CallRecord { static constexpr RecordType type{RecordType::CallToNative}; using CallRecord::CallRecord; RecordType getType() const override { return type; } std::vector defs() const override { auto defs = CallRecord::defs(); auto objs = CallRecord::getArgObjects(); defs.insert(defs.end(), objs.begin(), objs.end()); return defs; } }; struct GetOrSetPropertyNativeRecord : public Record { const ObjectID hostObjectID_; const std::string propName_; explicit GetOrSetPropertyNativeRecord( TimeSinceStart time, ObjectID hostObjectID, std::string propName) : Record(time), hostObjectID_(hostObjectID), propName_(propName) {} void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; std::vector uses() const override { return {hostObjectID_}; } protected: bool operator==(const Record &that) const override; }; /// A GetPropertyNativeRecord is an event where JS tries to access a property /// on a native object. /// This needs to be modeled as a call with no arguments, since native code /// can arbitrarily affect the JS heap during the accessor. struct GetPropertyNativeRecord final : public GetOrSetPropertyNativeRecord { static constexpr RecordType type{RecordType::GetPropertyNative}; using GetOrSetPropertyNativeRecord::GetOrSetPropertyNativeRecord; RecordType getType() const override { return type; } bool operator==(const Record &that) const final; }; struct GetPropertyNativeReturnRecord final : public Record, public ReturnMixin { static constexpr RecordType type{RecordType::GetPropertyNativeReturn}; GetPropertyNativeReturnRecord(TimeSinceStart time, TraceValue retVal) : Record(time), ReturnMixin(retVal) {} RecordType getType() const override { return type; } std::vector uses() const override { auto uses = Record::uses(); if (retVal_.isObject()) { uses.push_back(decodeObject(retVal_)); } return uses; } bool operator==(const Record &that) const final; protected: void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; }; /// A SetPropertyNativeRecord is an event where JS code writes to the property /// of a Native object. /// This needs to be modeled as a call with one argument, since native code /// can arbitrarily affect the JS heap during the accessor. struct SetPropertyNativeRecord final : public GetOrSetPropertyNativeRecord { static constexpr RecordType type{RecordType::SetPropertyNative}; TraceValue value_; explicit SetPropertyNativeRecord( TimeSinceStart time, ObjectID hostObjectID, std::string propName, TraceValue value) : GetOrSetPropertyNativeRecord(time, hostObjectID, propName), value_(value) {} bool operator==(const Record &that) const final; void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; RecordType getType() const override { return type; } std::vector defs() const override { auto defs = GetOrSetPropertyNativeRecord::defs(); if (value_.isObject()) { defs.push_back(decodeObject(value_)); } return defs; } }; /// A SetPropertyNativeReturnRecord needs to record no extra information struct SetPropertyNativeReturnRecord final : public Record { static constexpr RecordType type{RecordType::SetPropertyNativeReturn}; using Record::Record; RecordType getType() const override { return type; } bool operator==(const Record &that) const final { // Since there are no fields to compare, any two will always be the same. return Record::operator==(that); } }; /// A GetNativePropertyNamesRecord records an event where JS asked for a list /// of property names available on a host object. It records the object, and /// the returned list of property names. struct GetNativePropertyNamesRecord : public Record { static constexpr RecordType type{RecordType::GetNativePropertyNames}; const ObjectID hostObjectID_; explicit GetNativePropertyNamesRecord( TimeSinceStart time, ObjectID hostObjectID) : Record(time), hostObjectID_(hostObjectID) {} RecordType getType() const override { return type; } void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; std::vector uses() const override { return {hostObjectID_}; } bool operator==(const Record &that) const override; }; /// A GetNativePropertyNamesReturnRecord records what property names were /// returned by the GetNativePropertyNames query. struct GetNativePropertyNamesReturnRecord final : public Record { static constexpr RecordType type{RecordType::GetNativePropertyNamesReturn}; const std::vector propNames_; explicit GetNativePropertyNamesReturnRecord( TimeSinceStart time, const std::vector &propNames) : Record(time), propNames_(propNames) {} RecordType getType() const override { return type; } void toJSONInternal(::hermes::JSONEmitter &json, const SynthTrace &trace) const override; bool operator==(const Record &that) const override; }; /// Completes writing of the trace to the trace stream. If writing /// to a file, disables further writing to the file, or accumulation /// of data. void flushAndDisable(const ::hermes::vm::MockedEnvironment &env); }; llvm::raw_ostream &operator<<( llvm::raw_ostream &os, SynthTrace::RecordType type); std::istream &operator>>(std::istream &is, SynthTrace::RecordType &type); } // namespace tracing } // namespace hermes } // namespace facebook #endif // HERMESVM_API_TRACE #endif // HERMES_SYNTHTRACE_H