Skip to content

Commit

Permalink
RUMM-1679 Compress HTTP body using deflate format (ETF RFC 1950)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxep committed Oct 5, 2021
1 parent 3d4f6f2 commit 9e949eb
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 13 deletions.
8 changes: 8 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,8 @@
B3BBBCBC265E71D100943419 /* VitalMemoryReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */; };
B3FC3C0926526F0000DEED9E /* VitalInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */; };
B3FC3C3C2653A97700DEED9E /* VitalInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */; };
D2135330270CA722000315AD /* DataCompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213532F270CA722000315AD /* DataCompressionTests.swift */; };
D24C27EA270C8BEE005DE596 /* DataCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C27E9270C8BEE005DE596 /* DataCompression.swift */; };
D2F1B81126D795F3009F3293 /* DDNoopRUMMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81026D795F3009F3293 /* DDNoopRUMMonitor.swift */; };
D2F1B81326D8DA68009F3293 /* DDNoopRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81226D8DA68009F3293 /* DDNoopRUMMonitorTests.swift */; };
D2F1B81526D8E5FF009F3293 /* DDNoopTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */; };
Expand Down Expand Up @@ -1145,6 +1147,8 @@
B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReaderTests.swift; sourceTree = "<group>"; };
B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalInfo.swift; sourceTree = "<group>"; };
B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoTests.swift; sourceTree = "<group>"; };
D213532F270CA722000315AD /* DataCompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCompressionTests.swift; sourceTree = "<group>"; };
D24C27E9270C8BEE005DE596 /* DataCompression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCompression.swift; sourceTree = "<group>"; };
D2F1B81026D795F3009F3293 /* DDNoopRUMMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopRUMMonitor.swift; sourceTree = "<group>"; };
D2F1B81226D8DA68009F3293 /* DDNoopRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopRUMMonitorTests.swift; sourceTree = "<group>"; };
D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopTracerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1457,6 +1461,7 @@
61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */,
61133BB02423979B00786299 /* DataUploader.swift */,
61133BB12423979B00786299 /* DataUploadWorker.swift */,
D24C27E9270C8BEE005DE596 /* DataCompression.swift */,
61133BB22423979B00786299 /* HTTPClient.swift */,
61133BB42423979B00786299 /* RequestBuilder.swift */,
);
Expand Down Expand Up @@ -1709,6 +1714,7 @@
61133C312423990D00786299 /* DataUploadDelayTests.swift */,
61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */,
61133C322423990D00786299 /* DataUploaderTests.swift */,
D213532F270CA722000315AD /* DataCompressionTests.swift */,
61133C342423990D00786299 /* HTTPClientTests.swift */,
61133C332423990D00786299 /* RequestBuilderTests.swift */,
);
Expand Down Expand Up @@ -3902,6 +3908,7 @@
61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */,
61FF282424B8A1C3000B3D9B /* RUMEventFileOutput.swift in Sources */,
61B22E5A24F3E6B700DC26D2 /* RUMDebugging.swift in Sources */,
D24C27EA270C8BEE005DE596 /* DataCompression.swift in Sources */,
61C5A88B24509A0C00DA608C /* SpanOutput.swift in Sources */,
61133BE42423979B00786299 /* LogEncoder.swift in Sources */,
B3FC3C0926526F0000DEED9E /* VitalInfo.swift in Sources */,
Expand Down Expand Up @@ -3964,6 +3971,7 @@
61133C5A2423990D00786299 /* FileTests.swift in Sources */,
61AD4E3A24534075006E34EA /* TracingFeatureTests.swift in Sources */,
61133C6B2423990D00786299 /* LogMatcher.swift in Sources */,
D2135330270CA722000315AD /* DataCompressionTests.swift in Sources */,
61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */,
61F3CDAD25122C9200C816E5 /* UIKitRUMViewsHandlerTests.swift in Sources */,
61D980BC24E293F600E03345 /* RUMIntegrationsTests.swift in Sources */,
Expand Down
100 changes: 100 additions & 0 deletions Sources/Datadog/Core/Upload/DataCompression.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

import Foundation
import Compression
import zlib

/// Compresses the data into `ZLIB` data format.
///
/// The `Compression` library implements the zlib encoder at level 5 only. This compression level
/// provides a good balance between compression speed and compression ratio.
///
/// The encoded format is the ZLIB Compressed Data Format as described in IETF RFC 1950
/// https://datatracker.ietf.org/doc/html/rfc1950
///
/// - Parameter data: Source data to deflate
/// - Returns: The compressed data format.
internal func zip(_ data: Data) -> Data? {
// 2 bytes header - defines the compression mode
// +---+---+
// |CMF|FLG|
// +---+---+
// ref. https://datatracker.ietf.org/doc/html/rfc1950#section-2.2
let header = Data([0x78, 0x5e])

guard
let raw = deflate(data),
let checksum = adler32(data),
data.count > header.count + raw.count + checksum.count
else { return nil }

return header + raw + checksum
}

/// Compresses the data using the `ZLIB` compression algorithm.
///
/// The `Compression` library implements the zlib encoder at level 5 only. This compression level
/// provides a good balance between compression speed and compression ratio.
///
/// The encoded format is the raw DEFLATE format as described in in IETF RFC 1951
/// https://datatracker.ietf.org/doc/html/rfc1951
///
/// This deflate implementation uses `compression_encode_buffer(_:_:_:_:_:_:)`
/// from the `Compression` framework by allocating a destination buffer of source size and copying
/// the result into a `Data` structure. In the worst possible case, where the compression expands the
/// data size, the destination buffer becomes too small and deflation returns `nil`.
///
/// ref. https://developer.apple.com/documentation/compression/1480986-compression_encode_buffer
///
/// - Parameter data: Source data to deflate
/// - Returns: The compressed data. If the compressed data size is bigger than the source size,
/// or an error occurs, `nil` is returned.
internal func deflate(_ data: Data) -> Data? {
return data.withUnsafeBytes {
guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else {
return nil
}

let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: data.count)
defer { buffer.deallocate() }

// The number of bytes written to the destination buffer after compressing
// the input. If the funtion can't compress the entire input to fit into
// the provided destination buffer, or an error occurs, 0 is returned.
let size = compression_encode_buffer(buffer, data.count, ptr, data.count, nil, COMPRESSION_ZLIB)
guard size > 0 else {
return nil
}

return Data(bytes: buffer, count: size)
}
}

/// Calculates the Adler32 checksum of the given data.
///
/// An Adler-32 checksum is almost as reliable as a CRC-32 but can be computed much faster.
///
/// - Parameter data: Data to compute the checksum.
/// - Returns: The Adler-32 checksum.
internal func adler32(_ data: Data) -> Data? {
let adler: uLong? = data.withUnsafeBytes {
guard let ptr = $0.bindMemory(to: Bytef.self).baseAddress else {
return nil
}

// The Adler-32 checksum should be initialized to 1 as described in
// https://datatracker.ietf.org/doc/html/rfc1950#section-8
return zlib.adler32(1, ptr, uInt(data.count))
}

guard let adler = adler else {
return nil
}

var checksum = UInt32(adler).bigEndian
return Data(bytes: &checksum, count: MemoryLayout<UInt32>.size)
}
11 changes: 9 additions & 2 deletions Sources/Datadog/Core/Upload/RequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal struct RequestBuilder {

struct HTTPHeader {
static let contentTypeHeaderField = "Content-Type"
static let contentEncodingHeaderField = "Content-Encoding"
static let userAgentHeaderField = "User-Agent"
static let ddAPIKeyHeaderField = "DD-API-KEY"
static let ddEVPOriginHeaderField = "DD-EVP-ORIGIN"
Expand Down Expand Up @@ -126,9 +127,15 @@ internal struct RequestBuilder {
computedHeaders.forEach { field, value in headers[field] = value() }

request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = data

if let body = zip(data) {
headers[HTTPHeader.contentEncodingHeaderField] = "deflate"
request.httpBody = body
} else {
request.httpBody = data
}

request.allHTTPHeaderFields = headers
return request
}
}
64 changes: 64 additions & 0 deletions Tests/DatadogTests/Datadog/Core/Upload/DataCompressionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

import XCTest
@testable import Datadog

class DataCompressionTests: XCTestCase {
func testFuzzy_Adler32() {
for _ in 1...500 {
// Given
let data: Data = .mockRandom(ofSize: Int.mockRandom(min: 1, max: 10_000))

// When
let checksum = adler32(data)

// Then
XCTAssertEqual(checksum?.count, 4)
}
}

func testFuzzy_deflate_inflate() throws {
for _ in 1...500 {
// Given
let data: Data = .mockRandom(ofSize: Int.mockRandom(min: 100, max: 10_000))

// When
let compressed = try XCTUnwrap(deflate(data))
let decompressed = inflate(compressed)

// Then
XCTAssertEqual(decompressed, data)
}
}

func testFuzzy_zip_unzip() throws {
for _ in 1...500 {
// Given
let data: Data = .mockRandom(ofSize: Int.mockRandom(min: 100, max: 10_000))

// When
let compressed = try XCTUnwrap(zip(data))
let decompressed = unzip(compressed)

// Then
XCTAssertEqual(decompressed, data)
}
}

func test8MB_deflate_inflate() throws {
// Given
let size = 1_024 * 1_024 * 8 // 8 MB
let data: Data = .mockRandom(ofSize: size)

// When
let compressed = try XCTUnwrap(deflate(data))
let decompressed = inflate(compressed, capacity: size)

// Then
XCTAssertEqual(decompressed, data)
}
}
34 changes: 30 additions & 4 deletions Tests/DatadogTests/Datadog/Core/Upload/RequestBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ class RequestBuilderTests: XCTestCase {

let request = builder.uploadRequest(with: .mockRandom())
XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Type"])
XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Encoding"])
XCTAssertNotNil(request.allHTTPHeaderFields?["User-Agent"])
XCTAssertNotNil(request.allHTTPHeaderFields?["DD-API-KEY"])
XCTAssertNotNil(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"])
XCTAssertNotNil(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"])
XCTAssertNotNil(request.allHTTPHeaderFields?["DD-REQUEST-ID"])
XCTAssertEqual(request.allHTTPHeaderFields?.count, 6)
XCTAssertEqual(request.allHTTPHeaderFields?.count, 7)
}

// MARK: - Request Method
Expand All @@ -136,10 +137,35 @@ class RequestBuilderTests: XCTestCase {

// MARK: - Request Data

func testItSetsDataAsHTTPBodyInProducedRequest() {
let randomData: Data = .mockRandom()
func testWhenBuildingRequestWithData_thenItDeflatesHTTPBody() throws {
// When
let size = 256
let randomData: Data = .mock(ofSize: size)
let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom())

let request = builder.uploadRequest(with: randomData)
let body = try XCTUnwrap(request.httpBody)

// Then
XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Encoding"])
XCTAssertLessThan(body.count, Int(size), "HTTP body must be compressed")
XCTAssertEqual(unzip(body), randomData)
}

func testWhenBuildingRequestWithSmallData_thenItDoesNotDeflateHTTPBody() throws {
// When
// In the worst possible case, where deflate would expand the data,
// deflation falls back to stored (uncompressed) data.
let size = 8 // Small data will most likely inflate with zlib.
let randomData: Data = .mock(ofSize: size)
let builder = RequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom())

let request = builder.uploadRequest(with: randomData)
XCTAssertEqual(request.httpBody, randomData)
let body = try XCTUnwrap(request.httpBody)

// Then
XCTAssertNil(request.allHTTPHeaderFields?["Content-Encoding"])
XCTAssertEqual(body.count, Int(size), "HTTP body must not be alterated")
XCTAssertEqual(body, randomData)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class InternalMonitoringFeatureTests: XCTestCase {
"""
)
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Encoding"], "deflate")
XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken)
XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomSource)
XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], sdkVersion)
Expand Down
3 changes: 2 additions & 1 deletion Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class LoggingFeatureTests: XCTestCase {
"""
)
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Encoding"], "deflate")
XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken)
XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomSource)
XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], sdkVersion)
Expand Down Expand Up @@ -110,7 +111,7 @@ class LoggingFeatureTests: XCTestCase {
logger.debug("log 2")
logger.debug("log 3")

let payload = server.waitAndReturnRequests(count: 1)[0].httpBody!
let payload = try XCTUnwrap(server.waitAndReturnRequests(count: 1)[0].httpBody)

// Expected payload format:
// `[log1JSON,log2JSON,log3JSON]`
Expand Down
Loading

0 comments on commit 9e949eb

Please sign in to comment.