// Copyright Epic Games, Inc. All Rights Reserved. #pragma once namespace UE::IoStore::HTTP { #if IAS_HTTP_WITH_PERF //////////////////////////////////////////////////////////////////////////////// class FStopwatch { public: uint64 GetInterval(uint32 i) const; void SendStart() { Impl(0); } void SendEnd() { Impl(1); } void RecvStart() { Impl(2); } void RecvEnd() { Impl(3); } private: void Impl(uint32 Index); uint64 Samples[4] = {}; uint32 Counts[2] = {}; }; //////////////////////////////////////////////////////////////////////////////// uint64 FStopwatch::GetInterval(uint32 i) const { if (i >= UE_ARRAY_COUNT(Samples) - 1) { return 0; } return Samples[i + 1] - Samples[i]; } //////////////////////////////////////////////////////////////////////////////// void FStopwatch::Impl(uint32 Index) { if (uint64& Out = Samples[Index]; Out == 0) { Out = FPlatformTime::Cycles64(); } Counts[Index >> 1] += !(Index & 1); } #endif // IAS_HTTP_WITH_PERF //////////////////////////////////////////////////////////////////////////////// static void Trace(const struct FActivity*, ETrace, uint32); static FLaneEstate* GActivityTraceEstate = LaneEstate_New({ .Name = "Iax/Activity", .Group = "Iax", .Channel = GetIaxTraceChannel(), .Weight = 11, }); //////////////////////////////////////////////////////////////////////////////// struct FActivity { struct FParams { FAnsiStringView Method; FAnsiStringView Path; FHost* Host = nullptr; char* Buffer = nullptr; uint32 ContentSizeEst = 0; uint16 BufferSize = 0; bool bIsKeepAlive = false; bool bAllowChunked = true; }; enum class EStage : int32 { Build = -1, Request = -2, Response = -3, Content = -4, Done = -5, Cancelled = -6, Failed = -7, }; FActivity(const FParams& Params); void AddHeader(FAnsiStringView Key, FAnsiStringView Value); FOutcome Tick(FHttpPeer& Peer, int32* MaxRecvSize=nullptr); void SetSink(FTicketSink&& InSink, UPTRINT Param); void SetDestination(FIoBuffer* InDest); void Cancel(); void Done(); void Fail(const FOutcome& Outcome); EStage GetStage() const; FTransactId GetTransactId() const; const FTransactRef& GetTransaction() const; FAnsiStringView GetMethod() const; FAnsiStringView GetPath() const; FHost* GetHost() const; void EnumerateHeaders(FResponse::FHeaderSink HeaderSink) const; UPTRINT GetSinkParam() const; FIoBuffer& GetContent() const; uint32 GetRemainingKiB() const; FOutcome GetError() const; #if IAS_HTTP_WITH_PERF const FStopwatch& GetStopwatch() const; #endif private: enum class EState : uint8 { Build, Send, RecvMessage, RecvStream, RecvContent, RecvDone, Completed, Cancelled, Failed, _Num, }; void CallSink(); void ChangeState(EState InState); FOutcome Transact(FHttpPeer& Peer); FOutcome Send(FHttpPeer& Peer); FOutcome RecvMessage(FHttpPeer& Peer); FOutcome RecvContent(FHttpPeer& Peer, int32& MaxRecvSize); FOutcome RecvStream(FHttpPeer& Peer, int32& MaxRecvSize); FOutcome Recv(FHttpPeer& Peer, int32& MaxRecvSize); EState State = EState::Build; uint8 bAllowChunked : 1; uint8 _Unused : 3; uint8 LengthScore : 4; uint8 MethodOffset; uint8 PathOffset; uint16 PathLength; uint16 HeaderCount = 0; FTransactId TransactId = 0; #if IAS_HTTP_WITH_PERF FStopwatch Stopwatch; #endif union { FHost* Host; FIoBuffer* Dest; UPTRINT Error; }; UPTRINT SinkParam; FTicketSink Sink; FTransactRef Transaction; FBuffer Buffer; struct FHeaderRecord { int16 Key; uint16 ValueLength; char Data[]; }; friend void Trace(const FActivity*, ETrace, uint32); UE_NONCOPYABLE(FActivity); }; //////////////////////////////////////////////////////////////////////////////// FActivity::FActivity(const FParams& Params) : bAllowChunked(Params.bAllowChunked) , Host(Params.Host) , Buffer(Params.Buffer, Params.BufferSize) { check(Host != nullptr); // Make a copy of data. FAnsiStringView Path = Params.Path.IsEmpty() ? "/" : Params.Path; check(Path[0] == '/'); auto Copy = [this] (FAnsiStringView Value) { uint32 Length = uint32(Value.Len()); char* Ptr = Buffer.Alloc(Length); std::memcpy(Ptr, Value.GetData(), Length); uint32 Ret = uint32(ptrdiff_t(Ptr - Buffer.GetData())); check(Ret <= 255); return uint8(Ret); }; MethodOffset = Copy(Params.Method); PathOffset = Copy(Path); PathLength = uint16(Path.Len()); // Calculate a length score. uint32 ContentEstKiB = (Params.ContentSizeEst + 1023) >> 10; uint32 Pow2 = FMath::FloorLog2(uint32(ContentEstKiB)); Pow2 = FMath::Min(Pow2, 15u); LengthScore = uint8(Pow2); } //////////////////////////////////////////////////////////////////////////////// void FActivity::AddHeader(FAnsiStringView Key, FAnsiStringView Value) { check(State == EState::Build); check(HeaderCount < 0xffff); HeaderCount++; static_assert(alignof(FHeaderRecord) == alignof(uint16)); uint32 Count = sizeof(FHeaderRecord) + Key.Len() + Value.Len(); Count = (Count + sizeof(uint16) - 1) / sizeof(uint16); auto* Record = (FHeaderRecord*)(Buffer.Alloc(Count)); Record->Key = int16(Key.Len()); Record->ValueLength = int16(Value.Len()); char* Cursor = Record->Data; std::memcpy(Cursor, Key.GetData(), Key.Len()); std::memcpy(Cursor + Key.Len(), Value.GetData(), Value.Len()); } //////////////////////////////////////////////////////////////////////////////// void FActivity::SetSink(FTicketSink&& InSink, UPTRINT Param) { Sink = MoveTemp(InSink); SinkParam = Param; } //////////////////////////////////////////////////////////////////////////////// void FActivity::SetDestination(FIoBuffer* InDest) { Dest = InDest; } //////////////////////////////////////////////////////////////////////////////// void FActivity::Cancel() { if (State >= EState::Completed) { return; } ChangeState(EState::Cancelled); CallSink(); } //////////////////////////////////////////////////////////////////////////////// void FActivity::Done() { ChangeState(EState::RecvDone); CallSink(); ChangeState(EState::Completed); } //////////////////////////////////////////////////////////////////////////////// void FActivity::Fail(const FOutcome& Outcome) { if (State == EState::Failed) { return; } static_assert(sizeof(Outcome) == sizeof(Error)); std::memcpy(&Error, &Outcome, sizeof(Error)); ChangeState(EState::Failed); CallSink(); } //////////////////////////////////////////////////////////////////////////////// void FActivity::ChangeState(EState InState) { Trace(this, ETrace::StateChange, uint32(InState)); check(State != InState); State = InState; } //////////////////////////////////////////////////////////////////////////////// void FActivity::CallSink() { static uint32 Scope = LaneTrace_NewScope("Iax/Sink"); FLaneTrace* Lane = LaneEstate_Lookup(GActivityTraceEstate, this); FLaneTraceScope _(Lane, Scope); FTicketStatus& SinkArg = *(FTicketStatus*)this; Sink(SinkArg); } //////////////////////////////////////////////////////////////////////////////// FActivity::EStage FActivity::GetStage() const { switch (State) { case EState::Build: return EStage::Build; case EState::Send: case EState::RecvMessage: return EStage::Response; case EState::RecvContent: case EState::RecvStream: case EState::RecvDone: return EStage::Content; case EState::Completed: return EStage::Done; case EState::Cancelled: return EStage::Cancelled; case EState::Failed: return EStage::Failed; default: break; } return EStage::Failed; } //////////////////////////////////////////////////////////////////////////////// FTransactId FActivity::GetTransactId() const { check(TransactId != 0); return TransactId; } //////////////////////////////////////////////////////////////////////////////// const FTransactRef& FActivity::GetTransaction() const { check(State >= EState::Send && State <= EState::Completed); check(Transaction.IsValid()); return Transaction; } //////////////////////////////////////////////////////////////////////////////// FAnsiStringView FActivity::GetMethod() const { return { Buffer.GetData() + MethodOffset, PathOffset - MethodOffset }; } //////////////////////////////////////////////////////////////////////////////// FAnsiStringView FActivity::GetPath() const { return { Buffer.GetData() + PathOffset, PathLength }; } //////////////////////////////////////////////////////////////////////////////// FHost* FActivity::GetHost() const { check(State <= EState::RecvMessage); return Host; } //////////////////////////////////////////////////////////////////////////////// void FActivity::EnumerateHeaders(FResponse::FHeaderSink HeaderSink) const { UPTRINT RecordAddr = uint32(PathOffset) + uint32(PathLength); for (uint32 i = 0, n = HeaderCount; i < n; ++i) { RecordAddr = (RecordAddr + alignof(FHeaderRecord) - 1) & ~(alignof(FHeaderRecord) - 1); const auto* Record = (FHeaderRecord*)(Buffer.GetData() + RecordAddr); FAnsiStringView Key(Record->Data, Record->Key); FAnsiStringView Value(Record->Data + Record->Key, Record->ValueLength); HeaderSink(Key, Value); RecordAddr += sizeof(FHeaderRecord) + Record->Key + Record->ValueLength; } } //////////////////////////////////////////////////////////////////////////////// UPTRINT FActivity::GetSinkParam() const { return SinkParam; } //////////////////////////////////////////////////////////////////////////////// FIoBuffer& FActivity::GetContent() const { check(State == EState::RecvContent || State == EState::RecvStream); return *Dest; } //////////////////////////////////////////////////////////////////////////////// uint32 FActivity::GetRemainingKiB() const { if (State <= EState::RecvStream) return MAX_uint32; if (State > EState::RecvContent) return 0; return uint32(GetTransaction()->GetRemaining() >> 10); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::GetError() const { FOutcome Outcome = FOutcome::None(); void* Ptr = &Outcome; std::memcpy(Ptr, &Error, sizeof(Outcome)); return Outcome; } //////////////////////////////////////////////////////////////////////////////// #if IAS_HTTP_WITH_PERF const FStopwatch& FActivity::GetStopwatch() const { return Stopwatch; } #endif // IAS_HTTP_WITH_PERF //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::Transact(FHttpPeer& Peer) { Transaction = Peer.Transact(); if (!Transaction.IsValid()) { return FOutcome::Error("Unable to create a suitable transaction"); } const FHost* TheHost = GetHost(); bool bKeepAlive = TheHost->IsPooled(); Transaction->Begin(TheHost->GetHostName(), GetMethod(), GetPath()); EnumerateHeaders([this] (FAnsiStringView Key, FAnsiStringView Value) { Transaction->AddHeader(Key, Value); return true; }); TransactId = Transaction->End(bKeepAlive); ChangeState(EState::Send); return FOutcome::Ok(); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::Send(FHttpPeer& Peer) { #if IAS_HTTP_WITH_PERF Stopwatch.SendStart(); #endif TRACE_CPUPROFILER_EVENT_SCOPE(IasHttp::DoSend); FOutcome Outcome = Transaction->TrySendRequest(Peer); if (Outcome.IsError()) { Fail(Outcome); return Outcome; } if (Outcome.IsWaiting()) { return Outcome; } check(Outcome.IsOk()); #if IAS_HTTP_WITH_PERF Stopwatch.SendEnd(); #endif ChangeState(EState::RecvMessage); return FOutcome::Ok(int32(EStage::Request)); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::RecvMessage(FHttpPeer& Peer) { TRACE_CPUPROFILER_EVENT_SCOPE(IasHttp::DoRecvMessage); Trace(this, ETrace::StateChange, uint32(State)); #if IAS_HTTP_WITH_PERF Stopwatch.RecvStart(); #endif FOutcome Outcome = Transaction->TryRecvResponse(Peer); if (Outcome.IsError()) { Fail(Outcome); return Outcome; } if (Outcome.IsWaiting()) { return Outcome; } check(Outcome.IsOk()); bool bChunked = Transaction->IsChunked(); int64 ContentLength = Transaction->GetContentLength(); // Validate that the server's told us how and how much it will transmit if (bChunked) { if (bAllowChunked == 0) { Outcome = FOutcome::Error("Chunked transfer encoding disabled (ERRNOCHUNK)"); Fail(Outcome); return Outcome; } } else if (ContentLength < 0) { Outcome = FOutcome::Error("Invalid content length"); Fail(Outcome); return Outcome; } // Call out to the sink to get a content destination FIoBuffer* PriorDest = Dest; // to retain unioned Host ptr (redirect uses it in sink) CallSink(); // HEAD methods bool bHasBody = !GetMethod().Equals("HEAD", ESearchCase::IgnoreCase); bHasBody &= ((ContentLength > 0) | int32(Transaction->IsChunked())) == true; if (!bHasBody) { ChangeState(EState::RecvDone); return FOutcome::Ok(int32(EStage::Content)); } // Check the user gave us a destination for content if (Dest == PriorDest) { Outcome = FOutcome::Error("User did not provide a destination buffer"); Fail(Outcome); return Outcome; } // The user seems to have forgotten something. Let's help them along if (int32 DestSize = int32(Dest->GetSize()); DestSize == 0) { static const uint32 DefaultChunkSize = 4 << 10; uint32 Size = bChunked ? DefaultChunkSize : uint32(ContentLength); *Dest = FIoBuffer(Size); } else if (!bChunked && DestSize < ContentLength) { // todo: support piece-wise transfer of content (a la chunked). Outcome = FOutcome::Error("Destination buffer too small"); Fail(Outcome); return Outcome; } else if (enum { MinStreamBuf = 256 }; bChunked && DestSize < MinStreamBuf) { *Dest = FIoBuffer(MinStreamBuf); } // We're all set to go and get content check(Dest != nullptr); auto NextState = bChunked ? EState::RecvStream : EState::RecvContent; ChangeState(NextState); return FOutcome::Ok(int32(EStage::Response)); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::RecvContent(FHttpPeer& Peer, int32& MaxRecvSize) { TRACE_CPUPROFILER_EVENT_SCOPE(IasHttp::DoRecvContent); int64 Remaining = Transaction->GetRemaining(); FMutableMemoryView View = Dest->GetMutableView(); View = View.Right(Remaining).Left(MaxRecvSize); FOutcome Outcome = Transaction->TryRecv(View, Peer); if (Outcome.IsError()) { Fail(Outcome); return Outcome; } MaxRecvSize -= Outcome.GetResult(); if (Outcome.IsWaiting()) { return Outcome; } #if IAS_HTTP_WITH_PERF Stopwatch.RecvEnd(); #endif Done(); return FOutcome::Ok(int32(EStage::Content)); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::RecvStream(FHttpPeer& Peer, int32& MaxRecvSize) { TRACE_CPUPROFILER_EVENT_SCOPE(IasHttp::DoRecvStream); FMutableMemoryView View = Dest->GetMutableView(); View = View.Left(MaxRecvSize); MaxRecvSize -= int32(View.GetSize()); FOutcome Outcome = Transaction->TryRecv(View, Peer); if (Outcome.IsError()) { Fail(Outcome); return Outcome; } int32 Result = Outcome.GetResult(); if (Result != 0) { // Temporarily clamp IoBuffer so if the sink does GetView/GetSize() it // represents actual content and not the underlying working buffer. FIoBuffer& Outer = *Dest; FMemoryView SliceView = Outer.GetView(); SliceView = SliceView.Left(Result); FIoBuffer Slice(SliceView, Outer); Swap(Outer, Slice); CallSink(); Swap(Outer, Slice); } if (!Outcome.IsOk()) { check(Outcome.IsWaiting()); return Outcome; } #if IAS_HTTP_WITH_PERF Stopwatch.RecvEnd(); #endif *Dest = FIoBuffer(); Done(); return FOutcome::Ok(int32(EStage::Content)); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::Recv(FHttpPeer& Peer, int32& MaxRecvSize) { check(State >= EState::RecvMessage && State < EState::RecvDone); if (State == EState::RecvMessage) return RecvMessage(Peer); if (State == EState::RecvContent) return RecvContent(Peer, MaxRecvSize); if (State == EState::RecvStream) return RecvStream(Peer, MaxRecvSize); //-V547 check(false); // it is not expected that we'll get here return FOutcome::Error("unreachable"); } //////////////////////////////////////////////////////////////////////////////// FOutcome FActivity::Tick(FHttpPeer& Peer, int32* MaxRecvSize) { if (State == EState::Build) { FOutcome Outcome = Transact(Peer); if (!Outcome.IsOk()) { return Outcome; } } if (State == EState::Send) { return Send(Peer); } check(State > EState::Send && State < EState::RecvDone); check(MaxRecvSize != nullptr); return Recv(Peer, *MaxRecvSize); } //////////////////////////////////////////////////////////////////////////////// static void Trace(const struct FActivity* Activity, ETrace Action, uint32 Param) { if (Action == ETrace::ActivityCreate) { static uint32 ActScopes[16] = {}; if (ActScopes[0] == 0) { ActScopes[0] = LaneTrace_NewScope("Iax/Activity"); TAnsiStringBuilder<32> Builder; for (int32 i = 1, n = UE_ARRAY_COUNT(ActScopes); i < n; ++i) { Builder.Reset(); Builder << "Iax/Activity_"; Builder << (1ull << (i - 1)); ActScopes[i] = LaneTrace_NewScope(Builder); } } FLaneTrace* Lane = LaneEstate_Build(GActivityTraceEstate, Activity); LaneTrace_Enter(Lane, ActScopes[Activity->LengthScore]); return; } if (Action == ETrace::ActivityDestroy) { LaneEstate_Demolish(GActivityTraceEstate, Activity); return; } FLaneTrace* Lane = LaneEstate_Lookup(GActivityTraceEstate, Activity); if (Action == ETrace::StateChange) { static constexpr FAnsiStringView StateNames[] = { "Iax/Build", "Iax/WaitForSocket", "Iax/WaitResponse", "Iax/RecvStream", "Iax/RecvContent", "Iax/RecvDone", "Iax/Completed", "Iax/Cancelled", "Iax/Failed", }; static_assert(UE_ARRAY_COUNT(StateNames) == uint32(FActivity::EState::_Num)); static uint32 StateScopes[UE_ARRAY_COUNT(StateNames)] = {}; if (StateScopes[0] == 0) { for (int32 i = 0; FAnsiStringView Name : StateNames) { StateScopes[i++] = LaneTrace_NewScope(Name); } } uint32 Scope = StateScopes[Param]; if (Param == uint32(FActivity::EState::Build)) { LaneTrace_Enter(Lane, Scope); } else { LaneTrace_Change(Lane, Scope); } return; } } } // namespace UE::IoStore::HTTP