<reponame>lyqaiym/memory-leak-detector
#
# Copyright (C) 2021 ByteDance Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#!/usr/bin/python

import re
import os

import argparse
import commands


# addr2line environment
__ARMEABI_ADDR2LINE_FORMAT__ = "arm-linux-androideabi-addr2line -e %s -f %s"
__AARCH64_ADDR2LINE_FORMAT__ = "aarch64-linux-android-addr2line -e %s -f %s"


system_group = [
    'libhwui.so',
    'libsqlite.so',
    'WebViewGoogle.apk',
    'libstagefright.so',
    'libcamera_client.so',
    'libandroid_runtime.so'
]


symbol_table = {}
symbol_cache = {}


class Frame:
    def __init__(self, pc, path, desc):
        self.pc   = pc
        self.path = path
        self.desc = desc

    def __eq__(self, b):
        return self.pc == b.pc and self.path == b.path

    def __ne__(self, b):
        return self.pc != b.pc or self.path != b.path

    def __sub__(self, b):
        return 0 if self == b else (-1 if self.pc > b.pc else 1)


class Trace:
    def __init__(self, id, size, count, stack):
        self.id    = id
        self.size  = int(size)
        self.count = int(count)
        self.stack = stack

    def __eq__(self, b):
        if len(self.stack) != len(b.stack):
            return False
        for i in range(0, len(self.stack)):
            if self.stack[i] != b.stack[i]:
                return False
        return True

    def __sub__(self, b):
        if len(self.stack) != len(b.stack):
            return -1 if len(self.stack) > len(b.stack) else 1
        for i in range(0, len(self.stack)):
            if self.stack[i] != b.stack[i]:
                return self.stack[i] - b.stack[i]
        return 0


def addr_to_line(address, symbol_path):
    status, output = commands.getstatusoutput('arm-linux-androideabi-addr2line -e %s -f %s' % (symbol_path, address))
    if status != 0:
        raise Exception('execute [arm-linux-androideabi-addr2line -e %s -f %s] failed' % (symbol_path, address))
    splits = output.split('\n')

    status, output = commands.getstatusoutput('c++filt -n %s' % (splits[0]))
    if status != 0:
        raise Exception('execute [filt -n %s] failed' % (splits[0]))
    return output


def retry_symbol(record):
    for frame in record.stack:
        match = re.match(r'.+\/(.+\.so)$', frame.path, re.M | re.I)
        if not match or match.group(1) not in symbol_table.keys():
            continue

        caches = symbol_cache.get(match.group(1))
        if not caches:
            caches = {}
            symbol_cache.update({match.group(1): caches})

        symbol = caches.get(frame.pc)
        if not symbol:
            symbol = addr_to_line(frame.pc, symbol_table.get(match.group(1)))
            caches.update({frame.pc: (symbol if symbol else "unknown")})

        frame.desc = (symbol if symbol else "unknown")


def group_record(record):
    default = None
    for i in range(len(record.stack) - 1, -1, -1):
        match = re.match(r'.+\/(.+\.(so|apk|oat))', record.stack[i].path, re.M | re.I)
        if not match or match.group(1) == 'libraphael.so':
            continue
        elif record.stack[i].path.startswith('/data/'):
            return match.group(1)
        elif match.group(1) in system_group and not default:
            default = match.group(1)
    return default if default else 'extras'


def print_report(writer, report):
    groups = {}
    totals = 0
    for record in report:
        name = group_record(record)
        size = record.size
        groups.update({name: size + (int(groups.get(name)) if groups.has_key(name) else 0)})
        totals += size
    groups = sorted(groups.items(), lambda x, y: cmp(x[1], y[1]), reverse=True)

    writer.write('%s\t%s\n' % (format(totals, ',').rjust(13, ' '), 'totals'))
    extras = -1
    for i in range(0, len(groups)):
        if groups[i][0] != 'extras':
            writer.write('%s\t%s\n' % (format(groups[i][1], ',').rjust(13, ' '), groups[i][0]))
        else:
            extras = i
    if extras != -1:
        writer.write('%s\t%s\n' % (format(groups[extras][1], ',').rjust(13, ' '), groups[extras][0]))

    report.sort(lambda x, y: x.size - y.size, reverse=True)
    for record in report:
        retry_symbol(record)

        writer.write('\n%s, %s, %s\n' % (record.id, record.size, record.count))
        for frame in report[i].stack:
            writer.write('%s %s (%s)\n' % (frame.pc, frame.path, frame.desc))


def merge_report(report):
    merged = []
    report.sort(lambda x, y: x - y, reverse=True)

    record = report[0]
    for i in range(1, len(report)):
        if record == report[i]:
            record.size += report[i].size
            record.count += 1
        else:
            merged.append(record)
            record = report[i]
    merged.append(record)
    return merged


def parse_report(string):
    report = []
    splits = re.split(r'\n\n', string)
    for split in splits:
        match = re.compile(r'(0x[0-9a-f]+)\ (.+)\ \((.+)\)$', re.M | re.I).findall(split)
        stack = []
        for frame in match:
            stack.append(Frame(frame[0], frame[1], frame[2]))
        match = re.compile(r'(0x[0-9a-f]+),\ (\d+),\ (\d+)$', re.M | re.I).findall(split)
        report.append(Trace(match[0][0], match[0][1], match[0][2], stack))
    return report


def parse_symbol(path):
    for sub in os.listdir(path):
        if sub.endswith('.so'):
            symbol_table.update({sub: os.path.abspath(sub)})
    else:
        print(symbol_table)


if __name__ == '__main__':
    print('Raphael script version: 1.0.0')
    argParser = argparse.ArgumentParser()
    argParser.add_argument('-s', '--symbol', help='symbol folder path, symbol name must be the same as it in phone')
    argParser.add_argument('-o', '--output', help='output report name, the default output report name is leak.txt')
    argParser.add_argument('-r', '--report', help='report')
    argParams = argParser.parse_args()

    if not argParams.report:
        sys.exit(">>>>>>>> no report file")

    if not argParams.symbol:
        symbol_table = {}
    else:
        parse_symbol(argParams.symbol)

    reader = open(argParams.report)
    string = reader.read()
    reader.close()

    report = parse_report(string)
    report = merge_report(report)

    writer = open(argParams.output if argParams.output else 'leak-doubts.txt', 'w')
    print_report(writer, report)
    writer.close()

#
# Copyright (C) 2021 ByteDance Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#!/usr/bin/python

import re
import os

import argparse
import commands


# addr2line environment
__ARMEABI_ADDR2LINE_FORMAT__ = "arm-linux-androideabi-addr2line -e %s -f %s"
__AARCH64_ADDR2LINE_FORMAT__ = "aarch64-linux-android-addr2line -e %s -f %s"


system_group = [
    'libhwui.so',
    'libsqlite.so',
    'WebViewGoogle.apk',
    'libstagefright.so',
    'libcamera_client.so',
    'libandroid_runtime.so'
]


symbol_table = {}
symbol_cache = {}


class Frame:
    def __init__(self, pc, path, desc):
        self.pc   = pc
        self.path = path
        self.desc = desc

    def __eq__(self, b):
        return self.pc == b.pc and self.path == b.path

    def __ne__(self, b):
        return self.pc != b.pc or self.path != b.path

    def __sub__(self, b):
        return 0 if self == b else (-1 if self.pc > b.pc else 1)


class Trace:
    def __init__(self, id, size, count, stack):
        self.id    = id
        self.size  = int(size)
        self.count = int(count)
        self.stack = stack

    def __eq__(self, b):
        if len(self.stack) != len(b.stack):
            return False
        for i in range(0, len(self.stack)):
            if self.stack[i] != b.stack[i]:
                return False
        return True

    def __sub__(self, b):
        if len(self.stack) != len(b.stack):
            return -1 if len(self.stack) > len(b.stack) else 1
        for i in range(0, len(self.stack)):
            if self.stack[i] != b.stack[i]:
                return self.stack[i] - b.stack[i]
        return 0


def addr_to_line(address, symbol_path):
    status, output = commands.getstatusoutput('arm-linux-androideabi-addr2line -e %s -f %s' % (symbol_path, address))
    if status != 0:
        raise Exception('execute [arm-linux-androideabi-addr2line -e %s -f %s] failed' % (symbol_path, address))
    splits = output.split('\n')

    status, output = commands.getstatusoutput('c++filt -n %s' % (splits[0]))
    if status != 0:
        raise Exception('execute [filt -n %s] failed' % (splits[0]))
    return output


def retry_symbol(record):
    for frame in record.stack:
        match = re.match(r'.+\/(.+\.so)$', frame.path, re.M | re.I)
        if not match or match.group(1) not in symbol_table.keys():
            continue

        caches = symbol_cache.get(match.group(1))
        if not caches:
            caches = {}
            symbol_cache.update({match.group(1): caches})

        symbol = caches.get(frame.pc)
        if not symbol:
            symbol = addr_to_line(frame.pc, symbol_table.get(match.group(1)))
            caches.update({frame.pc: (symbol if symbol else "unknown")})

        frame.desc = (symbol if symbol else "unknown")


def group_record(record):
    default = None
    for i in range(len(record.stack) - 1, -1, -1):
        match = re.match(r'.+\/(.+\.(so|apk|oat))', record.stack[i].path, re.M | re.I)
        if not match or match.group(1) == 'libraphael.so':
            continue
        elif record.stack[i].path.startswith('/data/'):
            return match.group(1)
        elif match.group(1) in system_group and not default:
            default = match.group(1)
    return default if default else 'extras'


def print_report(writer, report):
    groups = {}
    totals = 0
    for record in report:
        name = group_record(record)
        size = record.size
        groups.update({name: size + (int(groups.get(name)) if groups.has_key(name) else 0)})
        totals += size
    groups = sorted(groups.items(), lambda x, y: cmp(x[1], y[1]), reverse=True)

    writer.write('%s\t%s\n' % (format(totals, ',').rjust(13, ' '), 'totals'))
    extras = -1
    for i in range(0, len(groups)):
        if groups[i][0] != 'extras':
            writer.write('%s\t%s\n' % (format(groups[i][1], ',').rjust(13, ' '), groups[i][0]))
        else:
            extras = i
    if extras != -1:
        writer.write('%s\t%s\n' % (format(groups[extras][1], ',').rjust(13, ' '), groups[extras][0]))

    report.sort(lambda x, y: x.size - y.size, reverse=True)
    for record in report:
        retry_symbol(record)

        writer.write('\n%s, %s, %s\n' % (record.id, record.size, record.count))
        for frame in report[i].stack:
            writer.write('%s %s (%s)\n' % (frame.pc, frame.path, frame.desc))


def merge_report(report):
    merged = []
    report.sort(lambda x, y: x - y, reverse=True)

    record = report[0]
    for i in range(1, len(report)):
        if record == report[i]:
            record.size += report[i].size
            record.count += 1
        else:
            merged.append(record)
            record = report[i]
    merged.append(record)
    return merged


def parse_report(string):
    report = []
    splits = re.split(r'\n\n', string)
    for split in splits:
        match = re.compile(r'(0x[0-9a-f]+)\ (.+)\ \((.+)\)$', re.M | re.I).findall(split)
        stack = []
        for frame in match:
            stack.append(Frame(frame[0], frame[1], frame[2]))
        match = re.compile(r'(0x[0-9a-f]+),\ (\d+),\ (\d+)$', re.M | re.I).findall(split)
        report.append(Trace(match[0][0], match[0][1], match[0][2], stack))
    return report


def parse_symbol(path):
    for sub in os.listdir(path):
        if sub.endswith('.so'):
            symbol_table.update({sub: os.path.abspath(sub)})
    else:
        print(symbol_table)


if __name__ == '__main__':
    print('Raphael script version: 1.0.0')
    argParser = argparse.ArgumentParser()
    argParser.add_argument('-s', '--symbol', help='symbol folder path, symbol name must be the same as it in phone')
    argParser.add_argument('-o', '--output', help='output report name, the default output report name is leak.txt')
    argParser.add_argument('-r', '--report', help='report')
    argParams = argParser.parse_args()

    if not argParams.report:
        sys.exit(">>>>>>>> no report file")

    if not argParams.symbol:
        symbol_table = {}
    else:
        parse_symbol(argParams.symbol)

    reader = open(argParams.report)
    string = reader.read()
    reader.close()

    report = parse_report(string)
    report = merge_report(report)

    writer = open(argParams.output if argParams.output else 'leak-doubts.txt', 'w')
    print_report(writer, report)
    writer.close()
# # Copyright (C) 2021 ByteDance Inc # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # #      http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # #!/usr/bin/python # addr2line environment