add an allocation counting integration test (#252)

Motivation:

In Swift, it's really easy to accidentally add allocations yet the
number of allocations is really important for performance.
Therefore it's useful to have an integration test that counts the number
of allocations and compares them to a reference (and fails if the number
of allocations increases).

Modifications:

- add an allocation counter
- add a HTTP1 client/server example that is run with the allocation
  counter intrumentation

Result:

We should now be able to track the number of allocations.
This commit is contained in:
Johannes Weiß 2018-03-29 18:31:08 +01:00 committed by GitHub
parent 224120c175
commit c84b19a399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 524 additions and 0 deletions

View File

@ -72,6 +72,7 @@ while getopts "f:" opt; do
esac
done
exec 3>&1 4>&2 # copy stdout/err to fd 3/4 to we can output control messages
cnt_ok=0
cnt_fail=0
for f in tests_*; do

View File

@ -36,3 +36,31 @@ function assert_equal_files() {
fail "file '$1' not equal to '$2'"
fi
}
function assert_less_than() {
if [[ ! "$1" -lt "$2" ]]; then
fail "assertion '$1' < '$2' failed"
fi
}
function assert_less_than_or_equal() {
if [[ ! "$1" -le "$2" ]]; then
fail "assertion '$1' <= '$2' failed"
fi
}
function assert_greater_than() {
if [[ ! "$1" -gt "$2" ]]; then
fail "assertion '$1' > '$2' failed"
fi
}
function assert_greater_than_or_equal() {
if [[ ! "$1" -ge "$2" ]]; then
fail "assertion '$1' >= '$2' failed"
fi
}
function warn() {
echo >&4 "warning: $*"
}

View File

@ -0,0 +1,14 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##

View File

@ -0,0 +1,74 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
source defines.sh
set -eu
swift_bin=swift
cp -R "test_01_resources/template"/* "$tmp/"
nio_root="$PWD/../.."
(
cd "$tmp"
function make_git_commit_all() {
git init
git config --local user.email does@really-not.matter
git config --local user.name 'Does Not Matter'
git add .
git commit -m 'everything'
}
cd HookedFree
make_git_commit_all
cd ..
cd AtomicCounter
make_git_commit_all
cd ..
mkdir swift-nio
cd swift-nio
cat > Package.swift <<"EOF"
// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(name: "swift-nio")
EOF
make_git_commit_all
cd ..
"$swift_bin" package edit --path "$nio_root" swift-nio
"$swift_bin" run -c release > "$tmp/output"
)
for test in 1000_reqs_1_conn; do
allocs=$(grep "$test:" "$tmp/output" | cut -d: -f2 | sed 's/ //g')
max_allowed_env_name="MAX_ALLOCS_ALLOWED_$test"
assert_greater_than "$allocs" 1000
if [[ -z "${!max_allowed_env_name+x}" ]]; then
if [[ -z "${!max_allowed_env_name+x}" ]]; then
warn "no reference number of allocations set (set to \$$max_allowed_env_name)"
warn "to set current number:"
warn " export $max_allowed_env_name=$allocs"
fi
else
assert_less_than_or_equal "$allocs" "${!max_allowed_env_name}"
fi
done

View File

@ -0,0 +1,36 @@
# Allocation Counting Test
This briefly describes how the allocation counting test works.
## How does it work?
Basically it's the world's cheapest allocation counter, actually it is a memory `free` counter rather than a `malloc` counter. Why does it count `free` and not `malloc`? Because it's much easier. A correct implementation of `malloc` without relying on the system's `malloc` is non-trivial, an implementation of `free` however is trivial: just do nothing. Sure, you wouldn't want to run a real-world program with a free function that doesn't free the memory as you would run out of memory really quickly. For a short benchmark however it doesn't matter. The only thing that the hooked `free` function does is incrementing an atomic integer variable (representing the number of allocations) than can be read out elsewhere later.
### How is the free function hooked?
Usually in UNIX it's enough to just define a function
```C
void free(void *ptr) { ... }
```
in the main binary and all modules will use this `free` function instead of the real one from the `libc`. For Linux, this is exactly what we're doing, the `bootstrap` binary defines such a `free` function in its `main.c`. On Darwin (macOS/iOS/...) however that is not the case and you need to use [dyld's interpose feature](https://books.google.co.uk/books?id=K8vUkpOXhN4C&lpg=PA73&ots=OMjhRWWwUu&dq=dyld%20interpose&pg=PA73#v=onepage&q=dyld%20interpose&f=false). The odd thing is that dyld's interposing _only_ works if it's in a `.dylib` and not from a binary's main executable. Therefore we need to build a slightly strange SwiftPM package:
- `bootstrap`: The main executable's main module (written in C) so we can hook the `free` function on Linux.
- `BootstrapSwift`: A SwiftPM module (written in Swift) called in from `bootstrap` which implements the actual SwiftNIO benchmark (and therefore depends on the `NIO` module).
- `HookedFree`: A separate SwiftPM package that builds a shared library (`.so` on Linux, `.dylib` on Darwin) which contains the `replacement_free` function which just increments an atomic integer representing the number of allocations. On Darwin, we use `DYLD_INTERPOSE` in this module, interposing libc's `free` with our `replacement_free`. This needs to be a separate SwiftPM package as otherwise its code would just live inside of the `bootstrap` executable and the dyld interposing feature wouldn't work.
- `AtomicCounter`: SwiftPM package (written in C) that implements the atomic counters. It needs to be a separate package as both `BoostrapSwift` (to read the allocation counter) as well as `HookedFree` (to increment the allocation counter) depend on it.
## What benchmark is run?
We run a single TCP connection over which 1000 HTTP requests are made by a client written in NIO, responded to by a server also written in NIO. We re-run the benchmark 10 times and return the lowest number of allocations that has been made.
## Why do I have to set a baseline?
By default this test should always succeed as it doesn't actually compare the number of allocations to a certain number. The reason is that this number varies ever so slightly between operating systems and Swift versions. At the time of writing on macOS we got roughly 326k allocations and on Linux 322k allocations for 1000 HTTP requests & responses. To set a baseline simply run
```bash
export MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=327000
```
or similar to set the maximum number of allocations allowed. If the benchmark exceeds these allocations the test will fail.

View File

@ -0,0 +1,30 @@
// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackageDescription
let package = Package(
name: "AtomicCounter",
products: [
.library(name: "AtomicCounter", targets: ["AtomicCounter"]),
],
dependencies: [ ],
targets: [
.target(
name: "AtomicCounter",
dependencies: []),
]
)

View File

@ -0,0 +1,21 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#ifndef ATOMIC_COUNTER
void inc_free_counter(void);
void reset_free_counter(void);
long read_free_counter(void);
#endif

View File

@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#include <stdatomic.h>
#define MAKE_COUNTER(name) /*
*/ _Atomic long g_ ## name ## _counter = ATOMIC_VAR_INIT(0); /*
*/ void inc_ ## name ## _counter(void) { /*
*/ atomic_fetch_add_explicit(&g_ ## name ## _counter, 1, memory_order_relaxed); /*
*/ } /*
*/ /*
*/ void reset_ ## name ## _counter(void) { /*
*/ atomic_store_explicit(&g_ ## name ## _counter, 0, memory_order_relaxed); /*
*/ } /*
*/ /*
*/ long read_ ## name ## _counter(void) { /*
*/ return atomic_load_explicit(&g_ ## name ## _counter, memory_order_relaxed); /*
*/ }
MAKE_COUNTER(free)

View File

@ -0,0 +1,30 @@
// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackageDescription
let package = Package(
name: "HookedFree",
products: [
.library(name: "HookedFree", type: .dynamic, targets: ["HookedFree"]),
],
dependencies: [
.package(url: "../AtomicCounter/", .branch("master")),
],
targets: [
.target(name: "HookedFree", dependencies: ["AtomicCounter"]),
]
)

View File

@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#ifndef HOOKED_FREE
#define HOOKED_FREE
void replacement_free(void *ptr);
#endif

View File

@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#include <hooked-free.h>
#include <stdlib.h>
#include <atomic-counter.h>
#define DYLD_INTERPOSE(_replacement,_replacee) \
__attribute__((used)) static struct { const void *replacement; const void *replacee; } _interpose_##_replacee \
__attribute__ ((section("__DATA,__interpose"))) = { (const void *)(unsigned long)&_replacement, (const void *)(unsigned long)&_replacee };
void replacement_free(void *ptr) {
if (ptr) {
inc_free_counter();
}
}
#if __APPLE__
DYLD_INTERPOSE(replacement_free, free)
#endif

View File

@ -0,0 +1,20 @@
// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "swift-malloc-info",
dependencies: [
.package(url: "HookedFree/", .branch("master")),
.package(url: "swift-nio/", .branch("master")),
],
targets: [
.target(
name: "SwiftBootstrap",
dependencies: ["NIO", "NIOHTTP1"]),
.target(
name: "bootstrap",
dependencies: ["SwiftBootstrap", "HookedFree"]),
]
)

View File

@ -0,0 +1,157 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIO
import NIOHTTP1
import Foundation
import AtomicCounter
private final class SimpleHTTPServer: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
typealias OutboundOut = HTTPServerResponsePart
private let bodyLength = 100
private let numberOfAdditionalHeaders = 3
private var responseHead: HTTPResponseHead {
var head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok)
head.headers.add(name: "Content-Length", value: "\(self.bodyLength)")
for i in 0..<self.numberOfAdditionalHeaders {
head.headers.add(name: "X-Random-Extra-Header", value: "\(i)")
}
return head
}
private func responseBody(allocator: ByteBufferAllocator) -> ByteBuffer {
var buffer = allocator.buffer(capacity: self.bodyLength)
for i in 0..<self.bodyLength {
buffer.write(integer: UInt8(i % Int(UInt8.max)))
}
return buffer
}
public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
if case .head(let req) = self.unwrapInboundIn(data), req.uri == "/allocation-test-1" {
ctx.write(self.wrapOutboundOut(.head(self.responseHead)), promise: nil)
ctx.write(self.wrapOutboundOut(.body(.byteBuffer(self.responseBody(allocator: ctx.channel.allocator)))), promise: nil)
ctx.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
}
}
}
@_cdecl("swift_main")
public func swiftMain() -> Int {
final class RepeatedRequests: ChannelInboundHandler {
typealias InboundIn = HTTPClientResponsePart
typealias OutboundOut = HTTPClientRequestPart
private let numberOfRequests: Int
private var remainingNumberOfRequests: Int
private let isDonePromise: EventLoopPromise<Int>
static var requestHead: HTTPRequestHead {
var head = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1), method: .GET, uri: "/allocation-test-1")
head.headers.add(name: "Host", value: "foo-\(ObjectIdentifier(self)).com")
return head
}
init(numberOfRequests: Int, eventLoop: EventLoop) {
self.remainingNumberOfRequests = numberOfRequests
self.numberOfRequests = numberOfRequests
self.isDonePromise = eventLoop.newPromise()
}
func wait() throws -> Int {
let reqs = try self.isDonePromise.futureResult.wait()
precondition(reqs == self.numberOfRequests)
return reqs
}
func errorCaught(ctx: ChannelHandlerContext, error: Error) {
ctx.channel.close(promise: nil)
self.isDonePromise.fail(error: error)
}
func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
let respPart = self.unwrapInboundIn(data)
if case .end(nil) = respPart {
if self.remainingNumberOfRequests <= 0 {
ctx.channel.close().map { self.numberOfRequests - self.remainingNumberOfRequests }.cascade(promise: self.isDonePromise)
} else {
self.remainingNumberOfRequests -= 1
ctx.write(self.wrapOutboundOut(.head(RepeatedRequests.requestHead)), promise: nil)
ctx.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
}
}
}
}
func measure(_ fn: () -> Int) -> [Int] {
func measureOne(_ fn: () -> Int) -> Int {
AtomicCounter.reset_free_counter()
_ = fn()
return AtomicCounter.read_free_counter()
}
_ = measureOne(fn) /* pre-heat and throw away */
var measurements: [Int] = []
for _ in 0..<10 {
measurements.append(measureOne(fn))
}
return measurements
}
func measureAndPrint(desc: String, fn: () -> Int) -> Void {
print("\(desc): ", terminator: "")
let measurements = measure(fn)
print(measurements.min() ?? -1)
}
measureAndPrint(desc: "1000_reqs_1_conn") {
let group = MultiThreadedEventLoopGroup(numThreads: System.coreCount)
defer {
try! group.syncShutdownGracefully()
}
let serverChannel = try! ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true).then {
channel.pipeline.add(handler: SimpleHTTPServer())
}
}.bind(host: "127.0.0.1", port: 0).wait()
defer {
try! serverChannel.close().wait()
}
let repeatedRequestsHandler = RepeatedRequests(numberOfRequests: 1000, eventLoop: group.next())
let clientChannel = try! ClientBootstrap(group: group)
.channelInitializer { channel in
channel.pipeline.addHTTPClientHandlers().then {
channel.pipeline.add(handler: repeatedRequestsHandler)
}
}
.connect(to: serverChannel.localAddress!)
.wait()
clientChannel.write(NIOAny(HTTPClientRequestPart.head(RepeatedRequests.requestHead)), promise: nil)
try! clientChannel.writeAndFlush(NIOAny(HTTPClientRequestPart.end(nil))).wait()
return try! repeatedRequestsHandler.wait()
}
return 0
}

View File

@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <atomic-counter.h>
#include <hooked-free.h>
#if !__APPLE__
void free(void *ptr) {
replacement_free(ptr);
}
#endif
void swift_main(void);
int main() {
swift_main();
}