swift-nio/dev/stackdiff-dtrace.py

178 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2020 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 sys
import re
import collections
num_regex = "^ +([0-9]+)$"
def put_in_dict(path):
# Our input looks something like:
#
# =====
# This will collect stack shots of allocations and print it when you exit dtrace.
# So go ahead, run your tests and then press Ctrl+C in this window to see the aggregated result
# =====
# DEBUG: After waiting 1 times, we quiesced to unfreeds=744
# test_1_reqs_1000_conn.total_allocations: 490000
# test_1_reqs_1000_conn.total_allocated_bytes: 42153000
# test_1_reqs_1000_conn.remaining_allocations: 0
# DEBUG: [["total_allocations": 490000, "total_allocated_bytes": 42153000, ...
#
# (truncated)
#
# libsystem_malloc.dylib`malloc_zone_malloc
# libswiftCore.dylib`swift_slowAlloc+0x28
# libswiftCore.dylib`swift_allocObject+0x27
# test_1_reqs_1000_conn`closure #3 in SelectableEventLoop.run()+0x166
# test_1_reqs_1000_conn`SelectableEventLoop.run()+0x234
# test_1_reqs_1000_conn`closure #1 in static MultiThreadedEventLoopGroup.setupThreadAndEventLoop(name:selectorFactory:initializer:)+0x12e
# test_1_reqs_1000_conn`partial apply for closure #1 in static MultiThreadedEventLoopGroup.setupThreadAndEventLoop(name:selectorFactory:initializer:)+0x25
# test_1_reqs_1000_conn`thunk for @escaping @callee_guaranteed (@guaranteed NIOThread) -> ()+0xf
# test_1_reqs_1000_conn`partial apply for thunk for @escaping @callee_guaranteed (@guaranteed NIOThread) -> ()+0x11
# test_1_reqs_1000_conn`closure #1 in static ThreadOpsPosix.run(handle:args:detachThread:)+0x1c9
# libsystem_pthread.dylib`_pthread_start+0xe0
# libsystem_pthread.dylib`thread_start+0xf
# 85945
#
# libsystem_malloc.dylib`malloc_zone_malloc
# libswiftCore.dylib`swift_slowAlloc+0x28
# libswiftCore.dylib`swift_allocObject+0x27
# (truncated)
dictionary = collections.defaultdict(list)
with open(path, "r") as f:
current_stack = []
for line in f:
if not line.startswith(" "):
# All lines we're intereted in are indented so ignore this one.
pass
elif re.match(num_regex, line):
# The line contains just a number. This must be the end of a
# stack, i.e. the number of allocations.
# Build a key for the current stack. Each line looks
# like: 'libswiftCore.dylib`swift_allocObject+0x27'. We want
# everything before the '+'. We only take at most the first 8
# lines for the key so that we group 'similar' stacks in our
# output.
key = "\n".join(line.split("+")[0] for line in current_stack[:8])
# Record this stack and reset our state to build a new one.
dictionary[key].append( (int(line), "\n".join(current_stack)) )
current_stack = []
else:
# This line doesn't contain just a number. This might be an
# entry in the current stack.
stripped = line.strip()
if stripped != "":
current_stack.append(stripped)
return dictionary
def total_count_for_key(d, key):
value = d[key]
return sum(map(lambda x : x[0], value))
def total_for_dictionary(d):
total = 0
for k in d.keys():
total += total_count_for_key(d, k)
return total
def extract_useful_keys(d):
keys = set()
for k in d.keys():
if total_count_for_key(d, k) >= 1000:
keys.add(k)
return keys
def print_dictionary_member(d, key):
print(total_count_for_key(d, key))
print(key)
print()
print_dictionary_member_detail(d, key)
print()
def print_dictionary_member_detail(d, key):
value = d[key]
for (count, stack) in value:
print(" %d" % count)
print(" " + stack.replace("\n", "\n "))
def usage():
print("Usage: stackdiff-dtrace.py OLD-STACKS NEW-STACKS")
print("")
print("stackdiff-dtrace can diff aggregated stack traces as produced")
print("by a `agg[ustack()] = count();` and printed with `printa(agg)`;")
print("by a dtrace program.")
print("")
print("Full example leveraging the malloc aggregation:")
print(" # get old stack traces")
print(" sudo malloc-aggregation.d -c ./old-binary > /tmp/old")
print(" # get new stack traces")
print(" sudo malloc-aggregation.d -c ./new-binary > /tmp/new")
print(" # diff them")
print(" stackdiff-dtrace.py /tmp/old /tmp/new")
if len(sys.argv) != 3:
usage()
sys.exit(1)
before_file = sys.argv[1]
after_file = sys.argv[2]
after_dict = put_in_dict(after_file)
before_dict = put_in_dict(before_file)
useful_after_keys = extract_useful_keys(after_dict)
useful_before_keys = extract_useful_keys(before_dict)
print("")
print("### only in AFTER")
only_after_total = 0
for x in sorted(list(useful_after_keys - useful_before_keys)):
print_dictionary_member(after_dict, x)
only_after_total += total_count_for_key(after_dict, x)
print("Total for only in AFTER: %d" % only_after_total)
print("")
print("### only in BEFORE")
only_before_total = 0
for x in sorted(list(useful_before_keys - useful_after_keys)):
print_dictionary_member(before_dict, x)
only_before_total += total_count_for_key(before_dict, x)
print("Total for only in BEFORE: %d" % only_before_total)
print("")
print("### different numbers")
for x in sorted(list(useful_before_keys & useful_after_keys)):
before_count = total_count_for_key(before_dict, x)
after_count = total_count_for_key(after_dict, x)
if before_count != after_count:
print("before: %d, after: %d" % (before_count,
after_count))
print(" AFTER")
print_dictionary_member_detail(after_dict, x)
print(" BEFORE")
print_dictionary_member_detail(before_dict, x)
print()
everything_before = total_for_dictionary(before_dict)
everything_after = total_for_dictionary(after_dict)
print("Total of _EVERYTHING_ BEFORE: %d, AFTER: %d, DIFFERENCE: %d" %
(everything_before, everything_after, everything_after - everything_before))