Skip to content

Commit

Permalink
Add FeatureObserver to metrics (#2167)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell authored Jun 11, 2024
1 parent 009b716 commit 137599c
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 1 deletion.
9 changes: 9 additions & 0 deletions src/workerd/api/blob.c++
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "blob.h"
#include "streams.h"
#include "util.h"
#include <workerd/io/observer.h>
#include <workerd/util/mimetype.h>

namespace workerd::api {
Expand Down Expand Up @@ -160,6 +161,11 @@ jsg::Ref<Blob> Blob::constructor(jsg::Lock& js,
return jsg::alloc<Blob>(js, concat(js, kj::mv(bits)), kj::mv(type));
}

kj::ArrayPtr<const byte> Blob::getData() const {
FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_GET_DATA);
return data;
}

jsg::Ref<Blob> Blob::slice(jsg::Optional<int> maybeStart, jsg::Optional<int> maybeEnd,
jsg::Optional<kj::String> type) {
int start = maybeStart.orDefault(0);
Expand Down Expand Up @@ -193,6 +199,7 @@ jsg::Ref<Blob> Blob::slice(jsg::Optional<int> maybeStart, jsg::Optional<int> may

jsg::Promise<kj::Array<kj::byte>> Blob::arrayBuffer(jsg::Lock& js) {
// TODO(perf): Find a way to avoid the copy.
FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_AS_ARRAY_BUFFER);
return js.resolvedPromise(kj::heapArray<byte>(data));
}

Expand All @@ -201,6 +208,7 @@ jsg::Promise<jsg::BufferSource> Blob::bytes(jsg::Lock& js) {
}

jsg::Promise<kj::String> Blob::text(jsg::Lock& js) {
FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_AS_TEXT);
return js.resolvedPromise(kj::str(data.asChars()));
}

Expand Down Expand Up @@ -260,6 +268,7 @@ private:
};

jsg::Ref<ReadableStream> Blob::stream() {
FeatureObserver::maybeRecordUse(FeatureObserver::Feature::BLOB_AS_STREAM);
return jsg::alloc<ReadableStream>(
IoContext::current(),
kj::heap<BlobInputStream>(JSG_THIS));
Expand Down
2 changes: 1 addition & 1 deletion src/workerd/api/blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Blob: public jsg::Object {
Blob(jsg::Lock& js, kj::Array<byte> data, kj::String type);
Blob(jsg::Ref<Blob> parent, kj::ArrayPtr<const byte> data, kj::String type);

inline kj::ArrayPtr<const byte> getData() const KJ_LIFETIMEBOUND { return data; }
kj::ArrayPtr<const byte> getData() const KJ_LIFETIMEBOUND;

// ---------------------------------------------------------------------------
// JS API
Expand Down
7 changes: 7 additions & 0 deletions src/workerd/io/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ wd_cc_capnp_library(
visibility = ["//visibility:public"],
deps = [
":compatibility_date_capnp",
":features_capnp",
":outcome_capnp",
":script_version_capnp",
":trimmed-supported-compatibility-date",
Expand Down Expand Up @@ -219,6 +220,12 @@ wd_cc_capnp_library(
visibility = ["//visibility:public"],
)

wd_cc_capnp_library(
name = "features_capnp",
srcs = ["features.capnp"],
visibility = ["//visibility:public"],
)

[kj_test(
src = f,
deps = [
Expand Down
32 changes: 32 additions & 0 deletions src/workerd/io/features.capnp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright (c) 2017-2022 Cloudflare, Inc.
# Licensed under the Apache 2.0 license found in the LICENSE file or at:
# https://opensource.org/licenses/Apache-2.0

@0x8b3d4aaa36221ec9;

using Cxx = import "/capnp/c++.capnp";
$Cxx.namespace("workerd");
$Cxx.allowCancellation;

enum Features {
test @0;
# A test feature that should never be used in production code.

# Due to a number of practical limitations on the metrics collection,
# we do not really want the list of features to grow unbounded over
# time. At any given point in time we shouldn't be trying to track
# more than 50 features at a time.
#
# Features we are no longer needing to track can and should be removed,
# just be careful to adjust the index ordinals of the remaining features
# correctly. In code, be sure to never rely on the ordinal value and
# instead always use the features enum to ensure that things won't break.

# We want to determine how users typically read the data from a Blob.
# The reason is so that we can determine how best to optimize the Blob
# implementation.
blobAsArrayBuffer @1;
blobAsText @2;
blobAsStream @3;
blobGetData @4;
}
26 changes: 26 additions & 0 deletions src/workerd/io/observer-test.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include "observer.h"
#include "worker-interface.h"
#include <kj/test.h>

namespace workerd {
namespace {

KJ_TEST("FeatureObserver") {
FeatureObserver::init(FeatureObserver::createDefault());

auto& observer = KJ_ASSERT_NONNULL(FeatureObserver::get());

observer.use(FeatureObserver::Feature::TEST);
observer.use(FeatureObserver::Feature::TEST);
observer.use(FeatureObserver::Feature::TEST);

uint64_t count = 0;
observer.collect([&](FeatureObserver::Feature feature, const uint64_t value) {
KJ_ASSERT(feature == FeatureObserver::Feature::TEST);
count = value;
});
KJ_ASSERT(count == 3);
}

} // namespace
} // namespace workerd
49 changes: 49 additions & 0 deletions src/workerd/io/observer.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "observer.h"
#include "worker-interface.h"
#include <kj/common.h>
#include <kj/map.h>
#include <kj/mutex.h>

namespace workerd {

namespace {
kj::Maybe<kj::Own<FeatureObserver>> featureObserver;

class FeatureObserverImpl final: public FeatureObserver {
public:
void use(Feature feature) const override {
auto lock = counts.lockExclusive();
lock->upsert(feature, 1, [](uint64_t& count, uint64_t value) {
count += value;
});
}

void collect(CollectCallback&& callback) const override {
auto lock = counts.lockShared();
for (auto& entry: *lock) {
callback(entry.key, entry.value);
}
}

private:
kj::MutexGuarded<kj::HashMap<Feature, uint64_t>> counts;
};

} // namespace

kj::Own<FeatureObserver> FeatureObserver::createDefault() {
return kj::heap<FeatureObserverImpl>();
}

void FeatureObserver::init(kj::Own<FeatureObserver> instance) {
KJ_ASSERT(featureObserver == kj::none);
featureObserver = kj::mv(instance);
}

kj::Maybe<FeatureObserver&> FeatureObserver::get() {
KJ_IF_SOME(impl, featureObserver) {
return *impl;
}
return kj::none;
}
};
32 changes: 32 additions & 0 deletions src/workerd/io/observer.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <kj/time.h>
#include <kj/compat/http.h>
#include <workerd/io/trace.h>
#include <workerd/io/features.capnp.h>
#include <workerd/jsg/observer.h>

namespace workerd {
Expand Down Expand Up @@ -247,4 +248,35 @@ class TeardownFinishedGuard {
Observer& ref;
};

// Provides counters/observers for various features. The intent is to
// make it possible to collect metrics on which runtime features are
// used and how often.
//
// There is exactly one instance of this class per worker process.
class FeatureObserver {
public:
static kj::Own<FeatureObserver> createDefault();
static void init(kj::Own<FeatureObserver> instance);
static kj::Maybe<FeatureObserver&> get();

// A "Feature" is just an opaque identifier defined in the features.capnp
// file.
using Feature = workerd::Features;

// Called to increment the usage counter for a feature.
virtual void use(Feature feature) const {}

using CollectCallback = kj::Function<void(Feature, const uint64_t)>;
// This method is called from the internal metrics collection mechanisn to harvest the
// current features and counts that have been recorded by the observer.
virtual void collect(CollectCallback&& callback) const {}

// Records the use of the feature if a FeatureObserver is available.
static inline void maybeRecordUse(Feature feature) {
KJ_IF_SOME(observer, get()) {
observer.use(feature);
}
}
};

} // namespace workerd

0 comments on commit 137599c

Please sign in to comment.