mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
1 Commits
bun-v1.3.5
...
jarred/int
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42401a4f2 |
@@ -78,6 +78,7 @@ src/bun.js/bindings/JSDOMWrapper.cpp
|
||||
src/bun.js/bindings/JSDOMWrapperCache.cpp
|
||||
src/bun.js/bindings/JSEnvironmentVariableMap.cpp
|
||||
src/bun.js/bindings/JSFFIFunction.cpp
|
||||
src/bun.js/bindings/JSHTTPStats.cpp
|
||||
src/bun.js/bindings/JSMockFunction.cpp
|
||||
src/bun.js/bindings/JSNextTickQueue.cpp
|
||||
src/bun.js/bindings/JSPropertyIterator.cpp
|
||||
|
||||
@@ -222,6 +222,7 @@ src/bun.js/webcore/Response.zig
|
||||
src/bun.js/webcore/S3Client.zig
|
||||
src/bun.js/webcore/S3File.zig
|
||||
src/bun.js/webcore/S3Stat.zig
|
||||
src/bun.js/webcore/ScriptExecutionContext.zig
|
||||
src/bun.js/webcore/Sink.zig
|
||||
src/bun.js/webcore/streams.zig
|
||||
src/bun.js/webcore/TextDecoder.zig
|
||||
|
||||
@@ -415,6 +415,7 @@ set(BUN_OBJECT_LUT_SOURCES
|
||||
${CWD}/src/bun.js/bindings/ProcessBindingNatives.cpp
|
||||
${CWD}/src/bun.js/modules/NodeModuleModule.cpp
|
||||
${CODEGEN_PATH}/ZigGeneratedClasses.lut.txt
|
||||
${CWD}/src/bun.js/bindings/JSHTTPStats.cpp
|
||||
)
|
||||
|
||||
set(BUN_OBJECT_LUT_OUTPUTS
|
||||
@@ -428,6 +429,7 @@ set(BUN_OBJECT_LUT_OUTPUTS
|
||||
${CODEGEN_PATH}/ProcessBindingNatives.lut.h
|
||||
${CODEGEN_PATH}/NodeModuleModule.lut.h
|
||||
${CODEGEN_PATH}/ZigGeneratedClasses.lut.h
|
||||
${CODEGEN_PATH}/JSHTTPStats.lut.h
|
||||
)
|
||||
|
||||
macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps)
|
||||
|
||||
58
packages/bun-types/globals.d.ts
vendored
58
packages/bun-types/globals.d.ts
vendored
@@ -1897,5 +1897,63 @@ declare namespace fetch {
|
||||
https?: boolean;
|
||||
},
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Statistics about fetch() & node:http client requests.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* console.log(fetch.stats);
|
||||
* // {
|
||||
* // requests: 10,
|
||||
* // bytesWritten: 1000,
|
||||
* // bytesRead: 500,
|
||||
* // fail: 1,
|
||||
* // redirect: 2,
|
||||
* // success: 7,
|
||||
* // timeout: 0,
|
||||
* // refused: 0,
|
||||
* // active: 0,
|
||||
* // }
|
||||
* ```
|
||||
*/
|
||||
export const stats: {
|
||||
/**
|
||||
* Total number of HTTP requests initiated since the process started
|
||||
*/
|
||||
readonly requests: number;
|
||||
/**
|
||||
* Total number of bytes written in HTTP requests across the process (including Worker threads)
|
||||
*/
|
||||
readonly bytesWritten: number;
|
||||
/**
|
||||
* Total number of bytes read from fetch responses across the process (including Worker threads)
|
||||
*/
|
||||
readonly bytesRead: number;
|
||||
/**
|
||||
* Number of HTTP requests that failed for any reason across the process (including Worker threads)
|
||||
*/
|
||||
readonly fail: number;
|
||||
/**
|
||||
* Number of HTTP requests that were redirected across the process (including Worker threads)
|
||||
*/
|
||||
readonly redirect: number;
|
||||
/**
|
||||
* Number of HTTP requests that succeeded across the process (including Worker threads)
|
||||
*/
|
||||
readonly success: number;
|
||||
/**
|
||||
* Number of HTTP requests that timed out across the process (including Worker threads)
|
||||
*/
|
||||
readonly timeout: number;
|
||||
/**
|
||||
* Number of HTTP requests that were refused by the server across the process (including Worker threads)
|
||||
*/
|
||||
readonly refused: number;
|
||||
/**
|
||||
* Number of HTTP requests currently in progress across the process (including Worker threads)
|
||||
*/
|
||||
readonly active: number;
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
#include "BunObjectModule.h"
|
||||
#include "JSCookie.h"
|
||||
#include "JSCookieMap.h"
|
||||
|
||||
#include "JSHTTPStats.h"
|
||||
#ifdef WIN32
|
||||
#include <ws2def.h>
|
||||
#else
|
||||
@@ -328,12 +328,14 @@ static JSValue constructPasswordObject(VM& vm, JSObject* bunObject)
|
||||
|
||||
JSValue constructBunFetchObject(VM& vm, JSObject* bunObject)
|
||||
{
|
||||
JSFunction* fetchFn = JSFunction::create(vm, bunObject->globalObject(), 1, "fetch"_s, Bun__fetch, ImplementationVisibility::Public, NoIntrinsic);
|
||||
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
|
||||
JSFunction* fetchFn = JSFunction::create(vm, globalObject, 1, "fetch"_s, Bun__fetch, ImplementationVisibility::Public, NoIntrinsic);
|
||||
|
||||
auto* globalObject = jsCast<Zig::GlobalObject*>(bunObject->globalObject());
|
||||
fetchFn->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "preconnect"_s), 1, Bun__fetchPreconnect, ImplementationVisibility::Public, NoIntrinsic,
|
||||
JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
fetchFn->putDirect(vm, JSC::Identifier::fromString(vm, "stats"_s), Bun::constructBunHTTPStatsObject(bunObject->globalObject()), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
return fetchFn;
|
||||
}
|
||||
|
||||
|
||||
120
src/bun.js/bindings/JSHTTPStats.cpp
Normal file
120
src/bun.js/bindings/JSHTTPStats.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "root.h"
|
||||
#include <JavaScriptCore/JSObject.h>
|
||||
#include <JavaScriptCore/JSString.h>
|
||||
#include <JavaScriptCore/JSFunction.h>
|
||||
#include <JavaScriptCore/JSGlobalObject.h>
|
||||
#include <JavaScriptCore/JSObject.h>
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
|
||||
namespace Bun {
|
||||
|
||||
using namespace JSC;
|
||||
|
||||
struct Bun__HTTPStats {
|
||||
std::atomic<uint64_t> total_requests;
|
||||
std::atomic<uint64_t> total_bytes_sent;
|
||||
std::atomic<uint64_t> total_bytes_received;
|
||||
std::atomic<uint64_t> total_requests_failed;
|
||||
std::atomic<uint64_t> total_requests_redirected;
|
||||
std::atomic<uint64_t> total_requests_succeeded;
|
||||
std::atomic<uint64_t> total_requests_timed_out;
|
||||
std::atomic<uint64_t> total_requests_connection_refused;
|
||||
};
|
||||
extern "C" Bun__HTTPStats Bun__HTTPStats;
|
||||
static_assert(std::atomic<uint64_t>::is_always_lock_free, "Bun__HTTPStats must be lock-free");
|
||||
|
||||
// clang-format off
|
||||
#define STATS_GETTER(name) \
|
||||
JSC_DEFINE_CUSTOM_GETTER(getStatsField_##name, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) \
|
||||
{ \
|
||||
return JSValue::encode(jsNumber(Bun__HTTPStats.name)); \
|
||||
} \
|
||||
\
|
||||
|
||||
#define FOR_EACH_STATS_FIELD(macro) \
|
||||
macro(total_requests) \
|
||||
macro(total_bytes_sent) \
|
||||
macro(total_bytes_received) \
|
||||
macro(total_requests_failed) \
|
||||
macro(total_requests_redirected) \
|
||||
macro(total_requests_succeeded) \
|
||||
macro(total_requests_timed_out) \
|
||||
macro(total_requests_connection_refused)
|
||||
|
||||
// clang-format on
|
||||
|
||||
FOR_EACH_STATS_FIELD(STATS_GETTER)
|
||||
|
||||
#undef STATS_GETTER
|
||||
#undef FOR_EACH_STATS_FIELD
|
||||
|
||||
extern "C" std::atomic<uint64_t> Bun__HTTPStats__total_requests_active;
|
||||
|
||||
JSC_DEFINE_CUSTOM_GETTER(getStatsField_total_requests_active, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))
|
||||
{
|
||||
return JSValue::encode(jsNumber(Bun__HTTPStats__total_requests_active));
|
||||
}
|
||||
|
||||
class JSHTTPStatsObject final : public JSNonFinalObject {
|
||||
public:
|
||||
using Base = JSNonFinalObject;
|
||||
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable;
|
||||
|
||||
template<typename CellType, SubspaceAccess>
|
||||
static GCClient::IsoSubspace* subspaceFor(VM& vm)
|
||||
{
|
||||
return &vm.plainObjectSpace();
|
||||
}
|
||||
|
||||
static JSHTTPStatsObject* create(VM& vm, Structure* structure)
|
||||
{
|
||||
JSHTTPStatsObject* object = new (NotNull, allocateCell<JSHTTPStatsObject>(vm)) JSHTTPStatsObject(vm, structure);
|
||||
object->finishCreation(vm);
|
||||
return object;
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
|
||||
static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
|
||||
{
|
||||
return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
|
||||
}
|
||||
|
||||
private:
|
||||
JSHTTPStatsObject(VM& vm, Structure* structure)
|
||||
: Base(vm, structure)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(VM& vm)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
}
|
||||
};
|
||||
|
||||
/* Source for JSHTTPStats.lut.h
|
||||
@begin jsHTTPStatsObjectTable
|
||||
requests getStatsField_total_requests CustomAccessor|ReadOnly|DontDelete
|
||||
active getStatsField_total_requests_active CustomAccessor|ReadOnly|DontDelete
|
||||
success getStatsField_total_requests_succeeded CustomAccessor|ReadOnly|DontDelete
|
||||
bytesWritten getStatsField_total_bytes_sent CustomAccessor|ReadOnly|DontDelete
|
||||
bytesRead getStatsField_total_bytes_received CustomAccessor|ReadOnly|DontDelete
|
||||
fail getStatsField_total_requests_failed CustomAccessor|ReadOnly|DontDelete
|
||||
redirect getStatsField_total_requests_redirected CustomAccessor|ReadOnly|DontDelete
|
||||
timeout getStatsField_total_requests_timed_out CustomAccessor|ReadOnly|DontDelete
|
||||
refused getStatsField_total_requests_connection_refused CustomAccessor|ReadOnly|DontDelete
|
||||
@end
|
||||
*/
|
||||
#include "JSHTTPStats.lut.h"
|
||||
|
||||
const ClassInfo JSHTTPStatsObject::s_info = { "HTTPStats"_s, &Base::s_info, &jsHTTPStatsObjectTable, nullptr, CREATE_METHOD_TABLE(JSHTTPStatsObject) };
|
||||
|
||||
JSC::JSObject* constructBunHTTPStatsObject(JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
|
||||
return JSHTTPStatsObject::create(vm, JSHTTPStatsObject::createStructure(vm, globalObject, globalObject->objectPrototype()));
|
||||
}
|
||||
|
||||
}
|
||||
4
src/bun.js/bindings/JSHTTPStats.h
Normal file
4
src/bun.js/bindings/JSHTTPStats.h
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
namespace Bun {
|
||||
JSC::JSObject* constructBunHTTPStatsObject(JSC::JSGlobalObject* globalObject);
|
||||
}
|
||||
232
src/http.zig
232
src/http.zig
@@ -116,6 +116,170 @@ pub const FetchRedirect = enum(u8) {
|
||||
});
|
||||
};
|
||||
|
||||
pub const Stats = extern struct {
|
||||
total_requests: std.atomic.Value(u64) = .init(0),
|
||||
total_bytes_sent: std.atomic.Value(u64) = .init(0),
|
||||
total_bytes_received: std.atomic.Value(u64) = .init(0),
|
||||
total_requests_failed: std.atomic.Value(u64) = .init(0),
|
||||
total_requests_redirected: std.atomic.Value(u64) = .init(0),
|
||||
total_requests_succeeded: std.atomic.Value(u64) = .init(0),
|
||||
total_requests_timed_out: std.atomic.Value(u64) = .init(0),
|
||||
total_requests_connection_refused: std.atomic.Value(u64) = .init(0),
|
||||
|
||||
pub var instance: Stats = .{};
|
||||
|
||||
pub fn addRequest() void {
|
||||
_ = instance.total_requests.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addBytesSent(bytes: u64) void {
|
||||
_ = instance.total_bytes_sent.fetchAdd(bytes, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addBytesReceived(bytes: u64) void {
|
||||
_ = instance.total_bytes_received.fetchAdd(bytes, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addRequestsFailed() void {
|
||||
_ = instance.total_requests_failed.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addRequestsRedirected() void {
|
||||
_ = instance.total_requests_redirected.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addRequestsSucceeded() void {
|
||||
_ = instance.total_requests_succeeded.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addRequestsTimedOut() void {
|
||||
_ = instance.total_requests_timed_out.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn addRequestsConnectionRefused() void {
|
||||
_ = instance.total_requests_connection_refused.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
pub fn fmt() Formatter {
|
||||
return .{
|
||||
.enable_color = bun.Output.enable_ansi_colors_stderr,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Formatter = struct {
|
||||
enable_color: bool = false,
|
||||
|
||||
pub fn format(this: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
const total_requests = Stats.instance.total_requests.load(.monotonic);
|
||||
const total_bytes_sent = Stats.instance.total_bytes_sent.load(.monotonic);
|
||||
const total_bytes_received = Stats.instance.total_bytes_received.load(.monotonic);
|
||||
const total_requests_failed = Stats.instance.total_requests_failed.load(.monotonic);
|
||||
const total_requests_redirected = Stats.instance.total_requests_redirected.load(.monotonic);
|
||||
const total_requests_succeeded = Stats.instance.total_requests_succeeded.load(.monotonic);
|
||||
const total_requests_timed_out = Stats.instance.total_requests_timed_out.load(.monotonic);
|
||||
const total_requests_connection_refused = Stats.instance.total_requests_connection_refused.load(.monotonic);
|
||||
const active_requests = AsyncHTTP.active_requests_count.load(.monotonic);
|
||||
var needs_space = false;
|
||||
|
||||
if (!(total_bytes_received > 0 or
|
||||
total_bytes_sent > 0 or
|
||||
total_requests > 0 or
|
||||
active_requests > 0 or
|
||||
total_requests_failed > 0 or
|
||||
total_requests_redirected > 0 or
|
||||
total_requests_succeeded > 0 or
|
||||
total_requests_timed_out > 0 or
|
||||
total_requests_connection_refused > 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.enable_color) {
|
||||
inline else => |enable_ansi_colors| {
|
||||
try writer.writeAll(Output.prettyFmt("\n <d>http stats | <r> ", enable_ansi_colors));
|
||||
needs_space = false;
|
||||
|
||||
if (active_requests > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
try writer.print(
|
||||
Output.prettyFmt("active<d>:<r> <blue><b>{}<r>", enable_ansi_colors),
|
||||
.{active_requests},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if (total_requests_succeeded > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
needs_space = true;
|
||||
try writer.print(
|
||||
Output.prettyFmt("ok<d>:<r> <green><b>{}<r>", enable_ansi_colors),
|
||||
.{total_requests_succeeded},
|
||||
);
|
||||
}
|
||||
|
||||
if (total_requests_failed > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
try writer.print(
|
||||
Output.prettyFmt("fail<d>:<r> <red><b>{}<r>", enable_ansi_colors),
|
||||
.{total_requests_failed},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if (total_bytes_received > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
try writer.print(
|
||||
Output.prettyFmt("recv<d>:<r> <b>{}<r>", enable_ansi_colors),
|
||||
.{bun.fmt.size(total_bytes_received, .{})},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if (total_bytes_sent > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
try writer.print(
|
||||
Output.prettyFmt("sent<d>:<r> <b>{}<r>", enable_ansi_colors),
|
||||
.{bun.fmt.size(total_bytes_sent, .{})},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if (total_requests_redirected > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
try writer.print(
|
||||
Output.prettyFmt("redirect<d>:<r> <yellow>{}<r>", enable_ansi_colors),
|
||||
.{total_requests_redirected},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if (total_requests_timed_out > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
needs_space = true;
|
||||
try writer.print(
|
||||
Output.prettyFmt("timeout<d>:<r> <b>{}<r>", enable_ansi_colors),
|
||||
.{total_requests_timed_out},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
if (total_requests_connection_refused > 0) {
|
||||
if (needs_space) try writer.writeAll(Output.prettyFmt(" <d>|<r> ", enable_ansi_colors));
|
||||
needs_space = true;
|
||||
try writer.print(
|
||||
Output.prettyFmt("refused<d>:<r> <red>{}<r>", enable_ansi_colors),
|
||||
.{total_requests_connection_refused},
|
||||
);
|
||||
needs_space = true;
|
||||
}
|
||||
|
||||
try writer.writeAll("\n");
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const HTTPRequestBody = union(enum) {
|
||||
bytes: []const u8,
|
||||
sendfile: Sendfile,
|
||||
@@ -411,7 +575,9 @@ const ProxyTunnel = struct {
|
||||
.tcp => |socket| socket.write(encoded_data, true),
|
||||
.none => 0,
|
||||
};
|
||||
const pending = encoded_data[@intCast(written)..];
|
||||
const written_bytes: usize = @intCast(@max(written, 0));
|
||||
Stats.addBytesSent(written_bytes);
|
||||
const pending = encoded_data[written_bytes..];
|
||||
if (pending.len > 0) {
|
||||
// lets flush when we are truly writable
|
||||
proxy.write_buffer.write(pending) catch bun.outOfMemory();
|
||||
@@ -489,6 +655,8 @@ const ProxyTunnel = struct {
|
||||
return;
|
||||
}
|
||||
const written = socket.write(encoded_data, true);
|
||||
const written_bytes: usize = @intCast(@max(written, 0));
|
||||
Stats.addBytesSent(written_bytes);
|
||||
if (written == encoded_data.len) {
|
||||
this.write_buffer.reset();
|
||||
return;
|
||||
@@ -1512,6 +1680,7 @@ pub const HTTPThread = struct {
|
||||
|
||||
{
|
||||
var batch_ = batch;
|
||||
_ = Stats.instance.total_requests.fetchAdd(batch.len, .monotonic);
|
||||
while (batch_.pop()) |task| {
|
||||
const http: *AsyncHTTP = @fieldParentPtr("task", task);
|
||||
this.queued_tasks.push(http);
|
||||
@@ -1746,12 +1915,14 @@ pub fn onTimeout(
|
||||
log("Timeout {s}\n", .{client.url.href});
|
||||
|
||||
defer NewHTTPContext(is_ssl).terminateSocket(socket);
|
||||
Stats.addRequestsTimedOut();
|
||||
client.fail(error.Timeout);
|
||||
}
|
||||
pub fn onConnectError(
|
||||
client: *HTTPClient,
|
||||
) void {
|
||||
log("onConnectError {s}\n", .{client.url.href});
|
||||
Stats.addRequestsConnectionRefused();
|
||||
client.fail(error.ConnectionRefused);
|
||||
}
|
||||
|
||||
@@ -1780,13 +1951,6 @@ pub inline fn cleanup(force: bool) void {
|
||||
default_arena.gc(force);
|
||||
}
|
||||
|
||||
pub const SOCKET_FLAGS: u32 = if (Environment.isLinux)
|
||||
SOCK.CLOEXEC | posix.MSG.NOSIGNAL
|
||||
else
|
||||
SOCK.CLOEXEC;
|
||||
|
||||
pub const OPEN_SOCKET_FLAGS = SOCK.CLOEXEC;
|
||||
|
||||
pub const extremely_verbose = false;
|
||||
|
||||
fn writeProxyConnect(
|
||||
@@ -2453,6 +2617,11 @@ pub const AsyncHTTP = struct {
|
||||
pub var active_requests_count = std.atomic.Value(usize).init(0);
|
||||
pub var max_simultaneous_requests = std.atomic.Value(usize).init(256);
|
||||
|
||||
comptime {
|
||||
// This is not part of Stats because it's used in other places
|
||||
@export(&active_requests_count, .{ .name = "Bun__HTTPStats__total_requests_active" });
|
||||
}
|
||||
|
||||
pub fn loadEnv(allocator: std.mem.Allocator, logger: *Log, env: *DotEnv.Loader) void {
|
||||
if (env.get("BUN_CONFIG_MAX_HTTP_REQUESTS")) |max_http_requests| {
|
||||
const max = std.fmt.parseInt(u16, max_http_requests, 10) catch {
|
||||
@@ -3272,7 +3441,9 @@ noinline fn sendInitialRequestPayload(this: *HTTPClient, comptime is_first_call:
|
||||
return error.WriteFailed;
|
||||
}
|
||||
|
||||
this.state.request_sent_len += @as(usize, @intCast(amount));
|
||||
const sent_bytes: usize = @intCast(@max(amount, 0));
|
||||
this.state.request_sent_len += sent_bytes;
|
||||
Stats.addBytesSent(sent_bytes);
|
||||
const has_sent_headers = this.state.request_sent_len >= headers_len;
|
||||
|
||||
if (has_sent_headers and this.verbose != .none) {
|
||||
@@ -3371,9 +3542,11 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s
|
||||
this.closeAndFail(error.WriteFailed, is_ssl, socket);
|
||||
return;
|
||||
}
|
||||
const sent_bytes: usize = @intCast(@max(amount, 0));
|
||||
|
||||
this.state.request_sent_len += @as(usize, @intCast(amount));
|
||||
this.state.request_body = this.state.request_body[@as(usize, @intCast(amount))..];
|
||||
Stats.addBytesSent(sent_bytes);
|
||||
this.state.request_sent_len += sent_bytes;
|
||||
this.state.request_body = this.state.request_body[sent_bytes..];
|
||||
|
||||
if (this.state.request_body.len == 0) {
|
||||
this.state.request_stage = .done;
|
||||
@@ -3391,8 +3564,10 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s
|
||||
this.closeAndFail(error.WriteFailed, is_ssl, socket);
|
||||
return;
|
||||
}
|
||||
this.state.request_sent_len += @as(usize, @intCast(amount));
|
||||
stream.buffer.cursor += @intCast(amount);
|
||||
const sent_bytes: usize = @intCast(@max(amount, 0));
|
||||
this.state.request_sent_len += sent_bytes;
|
||||
Stats.addBytesSent(sent_bytes);
|
||||
stream.buffer.cursor += sent_bytes;
|
||||
if (amount < to_send.len) {
|
||||
stream.has_backpressure = true;
|
||||
}
|
||||
@@ -3436,8 +3611,10 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s
|
||||
const to_send = this.state.request_body;
|
||||
const amount = proxy.writeData(to_send) catch return; // just wait and retry when onWritable! if closed internally will call proxy.onClose
|
||||
|
||||
this.state.request_sent_len += @as(usize, @intCast(amount));
|
||||
this.state.request_body = this.state.request_body[@as(usize, @intCast(amount))..];
|
||||
const sent_bytes: usize = @intCast(@max(amount, 0));
|
||||
this.state.request_sent_len += sent_bytes;
|
||||
Stats.addBytesSent(sent_bytes);
|
||||
this.state.request_body = this.state.request_body[sent_bytes..];
|
||||
|
||||
if (this.state.request_body.len == 0) {
|
||||
this.state.request_stage = .done;
|
||||
@@ -3452,8 +3629,10 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s
|
||||
if (stream.buffer.isNotEmpty()) {
|
||||
const to_send = stream.buffer.slice();
|
||||
const amount = proxy.writeData(to_send) catch return; // just wait and retry when onWritable! if closed internally will call proxy.onClose
|
||||
this.state.request_sent_len += amount;
|
||||
stream.buffer.cursor += @truncate(amount);
|
||||
const sent_bytes: usize = @intCast(@max(amount, 0));
|
||||
this.state.request_sent_len += sent_bytes;
|
||||
Stats.addBytesSent(sent_bytes);
|
||||
stream.buffer.cursor += sent_bytes;
|
||||
if (amount < to_send.len) {
|
||||
stream.has_backpressure = true;
|
||||
}
|
||||
@@ -3517,7 +3696,9 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s
|
||||
}
|
||||
}
|
||||
|
||||
this.state.request_sent_len += @as(usize, @intCast(amount));
|
||||
const sent_bytes: usize = @intCast(@max(amount, 0));
|
||||
this.state.request_sent_len += sent_bytes;
|
||||
Stats.addBytesSent(sent_bytes);
|
||||
const has_sent_headers = this.state.request_sent_len >= headers_len;
|
||||
|
||||
if (has_sent_headers and this.state.request_body.len > 0) {
|
||||
@@ -3676,6 +3857,12 @@ pub fn handleOnDataHeaders(
|
||||
if (this.flags.proxy_tunneling and this.proxy_tunnel == null) {
|
||||
// we are proxing we dont need to cloneMetadata yet
|
||||
this.startProxyHandshake(is_ssl, socket);
|
||||
|
||||
if (body_buf.len > 0) {
|
||||
if (this.proxy_tunnel) |proxy| {
|
||||
proxy.receiveData(body_buf);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3731,6 +3918,7 @@ pub fn onData(
|
||||
socket: NewHTTPContext(is_ssl).HTTPSocket,
|
||||
) void {
|
||||
log("onData {}", .{incoming_data.len});
|
||||
Stats.addBytesReceived(incoming_data.len);
|
||||
if (this.signals.get(.aborted)) {
|
||||
this.closeAndAbort(is_ssl, socket);
|
||||
return;
|
||||
@@ -3823,13 +4011,13 @@ fn fail(this: *HTTPClient, err: anyerror) void {
|
||||
this.state.response_stage = .fail;
|
||||
this.state.fail = err;
|
||||
this.state.stage = .fail;
|
||||
Stats.addRequestsFailed();
|
||||
|
||||
if (!this.flags.defer_fail_until_connecting_is_complete) {
|
||||
const callback = this.result_callback;
|
||||
const result = this.toResult();
|
||||
this.state.reset(this.allocator);
|
||||
this.flags.proxy_tunneling = false;
|
||||
|
||||
callback.run(@fieldParentPtr("client", this), result);
|
||||
}
|
||||
}
|
||||
@@ -3915,6 +4103,7 @@ pub fn progressUpdate(this: *HTTPClient, comptime is_ssl: bool, ctx: *NewHTTPCon
|
||||
this.state.request_stage = .done;
|
||||
this.state.stage = .done;
|
||||
this.flags.proxy_tunneling = false;
|
||||
Stats.addRequestsSucceeded();
|
||||
}
|
||||
|
||||
result.body.?.* = body;
|
||||
@@ -4647,6 +4836,7 @@ pub fn handleResponseMetadata(
|
||||
}
|
||||
}
|
||||
this.state.flags.is_redirect_pending = true;
|
||||
Stats.addRequestsRedirected();
|
||||
if (this.method.hasRequestBody()) {
|
||||
this.state.flags.resend_request_body_on_redirect = true;
|
||||
}
|
||||
@@ -4847,3 +5037,7 @@ pub const Headers = struct {
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
@export(&Stats.instance, .{ .name = "Bun__HTTPStats" });
|
||||
}
|
||||
|
||||
109
test/js/web/fetch/fetch-stats.test.ts
Normal file
109
test/js/web/fetch/fetch-stats.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import "harness";
|
||||
|
||||
describe("fetch.stats", () => {
|
||||
it("tracks request statistics", async () => {
|
||||
// Save initial stats
|
||||
const initialStats = {
|
||||
requests: fetch.stats.requests,
|
||||
bytesWritten: fetch.stats.bytesWritten,
|
||||
bytesRead: fetch.stats.bytesRead,
|
||||
success: fetch.stats.success,
|
||||
active: fetch.stats.active,
|
||||
fail: fetch.stats.fail,
|
||||
redirect: fetch.stats.redirect,
|
||||
timeout: fetch.stats.timeout,
|
||||
refused: fetch.stats.refused,
|
||||
};
|
||||
|
||||
// Start a server
|
||||
const responseBody = "Hello, World!";
|
||||
const requestBody = "Test request body";
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0, // Use any available port
|
||||
fetch(req) {
|
||||
return new Response(responseBody, {
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Make a fetch request with a body
|
||||
const response = await fetch(server.url, {
|
||||
method: "POST",
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
expect(responseText).toBe(responseBody);
|
||||
|
||||
// Verify stats were updated
|
||||
expect(fetch.stats.requests).toBe(initialStats.requests + 1);
|
||||
expect(fetch.stats.success).toBe(initialStats.success + 1);
|
||||
expect(fetch.stats.bytesWritten).toBeGreaterThan(initialStats.bytesWritten);
|
||||
expect(fetch.stats.bytesRead).toBeGreaterThan(initialStats.bytesRead);
|
||||
|
||||
// Active should return to the same value after request completes
|
||||
expect(fetch.stats.active).toBe(initialStats.active);
|
||||
});
|
||||
|
||||
it("tracks multiple concurrent requests", async () => {
|
||||
const initialActive = fetch.stats.active;
|
||||
const initialRequests = fetch.stats.requests;
|
||||
|
||||
// Start a server that delays responses
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch(req) {
|
||||
await Bun.sleep(50); // Small delay to ensure concurrent requests
|
||||
return new Response("OK");
|
||||
},
|
||||
});
|
||||
|
||||
// Start multiple requests without awaiting them
|
||||
const requests = Array.from({ length: 5 }, () => fetch(server.url).then(r => r.blob()));
|
||||
|
||||
// Check active requests increased
|
||||
expect(fetch.stats.active).toBeGreaterThan(initialActive);
|
||||
expect(fetch.stats.requests).toBe(initialRequests + 5);
|
||||
|
||||
// Wait for all requests to complete
|
||||
await Promise.all(requests);
|
||||
|
||||
// Active should return to initial value
|
||||
expect(fetch.stats.active).toBe(initialActive);
|
||||
});
|
||||
|
||||
it("tracks failed requests", async () => {
|
||||
const initialFail = fetch.stats.fail;
|
||||
|
||||
// Try to connect to a non-existent server
|
||||
try {
|
||||
await fetch("http://localhost:54321");
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
}
|
||||
|
||||
expect(fetch.stats.fail).toBe(initialFail + 1);
|
||||
});
|
||||
|
||||
it("has all expected properties", () => {
|
||||
const expectedProperties = [
|
||||
"requests",
|
||||
"bytesWritten",
|
||||
"bytesRead",
|
||||
"fail",
|
||||
"redirect",
|
||||
"success",
|
||||
"timeout",
|
||||
"refused",
|
||||
"active",
|
||||
] as const;
|
||||
|
||||
for (const prop of expectedProperties) {
|
||||
expect(fetch.stats).toHaveProperty(prop);
|
||||
expect(fetch.stats[prop]).toBeTypeOf("number");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user