Skip to content

Commit

Permalink
Implement memory usage profiling RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
ivankravets committed Jul 25, 2023
1 parent 65b31c6 commit a3ad310
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 45 deletions.
16 changes: 8 additions & 8 deletions platformio/builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import json
import os
import sys
from time import time
import time

import click
from SCons.Script import ARGUMENTS # pylint: disable=import-error
Expand Down Expand Up @@ -61,7 +61,7 @@
"piotarget",
"piolib",
"pioupload",
"piosize",
"piomemusage",
"pioino",
"piomisc",
"piointegration",
Expand All @@ -71,7 +71,7 @@
variables=clivars,
# Propagating External Environment
ENV=os.environ,
UNIX_TIME=int(time()),
UNIX_TIME=int(time.time()),
PYTHONEXE=get_pythonexe_path(),
)

Expand Down Expand Up @@ -183,7 +183,7 @@

# Checking program size
if env.get("SIZETOOL") and not (
set(["nobuild", "sizedata"]) & set(COMMAND_LINE_TARGETS)
set(["nobuild", "__memusage"]) & set(COMMAND_LINE_TARGETS)
):
env.Depends("upload", "checkprogsize")
# Replace platform's "size" target with our
Expand Down Expand Up @@ -235,16 +235,16 @@
)
env.Exit(0)

if "sizedata" in COMMAND_LINE_TARGETS:
if "__memusage" in COMMAND_LINE_TARGETS:
AlwaysBuild(
env.Alias(
"sizedata",
"__memusage",
DEFAULT_TARGETS,
env.VerboseAction(env.DumpSizeData, "Generating memory usage report..."),
env.VerboseAction(env.DumpMemoryUsage, "Generating memory usage report..."),
)
)

Default("sizedata")
Default("__memusage")

# issue #4604: process targets sequentially
for index, target in enumerate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,33 @@

# pylint: disable=too-many-locals

import json
import os
import sys
from os import environ, makedirs, remove
from os.path import isdir, join, splitdrive
import time

from elftools.elf.descriptions import describe_sh_flags
from elftools.elf.elffile import ELFFile

from platformio.compat import IS_WINDOWS
from platformio.proc import exec_command
from platformio.project.memusage import save_report


def _run_tool(cmd, env, tool_args):
sysenv = environ.copy()
sysenv = os.environ.copy()
sysenv["PATH"] = str(env["ENV"]["PATH"])

build_dir = env.subst("$BUILD_DIR")
if not isdir(build_dir):
makedirs(build_dir)
tmp_file = join(build_dir, "size-data-longcmd.txt")
if not os.path.isdir(build_dir):
os.makedirs(build_dir)
tmp_file = os.path.join(build_dir, "size-data-longcmd.txt")

with open(tmp_file, mode="w", encoding="utf8") as fp:
fp.write("\n".join(tool_args))

cmd.append("@" + tmp_file)
result = exec_command(cmd, env=sysenv)
remove(tmp_file)
os.remove(tmp_file)

return result

Expand Down Expand Up @@ -92,8 +92,8 @@ def _collect_sections_info(env, elffile):
}

sections[section.name] = section_data
sections[section.name]["in_flash"] = env.pioSizeIsFlashSection(section_data)
sections[section.name]["in_ram"] = env.pioSizeIsRamSection(section_data)
sections[section.name]["in_flash"] = env.memusageIsFlashSection(section_data)
sections[section.name]["in_ram"] = env.memusageIsRamSection(section_data)

return sections

Expand All @@ -106,7 +106,7 @@ def _collect_symbols_info(env, elffile, elf_path, sections):
sys.stderr.write("Couldn't find symbol table. Is ELF file stripped?")
env.Exit(1)

sysenv = environ.copy()
sysenv = os.environ.copy()
sysenv["PATH"] = str(env["ENV"]["PATH"])

symbol_addrs = []
Expand All @@ -117,7 +117,7 @@ def _collect_symbols_info(env, elffile, elf_path, sections):
symbol_size = s["st_size"]
symbol_type = symbol_info["type"]

if not env.pioSizeIsValidSymbol(s.name, symbol_type, symbol_addr):
if not env.memusageIsValidSymbol(s.name, symbol_type, symbol_addr):
continue

symbol = {
Expand All @@ -126,7 +126,7 @@ def _collect_symbols_info(env, elffile, elf_path, sections):
"name": s.name,
"type": symbol_type,
"size": symbol_size,
"section": env.pioSizeDetermineSection(sections, symbol_addr),
"section": env.memusageDetermineSection(sections, symbol_addr),
}

if s.name.startswith("_Z"):
Expand All @@ -144,8 +144,8 @@ def _collect_symbols_info(env, elffile, elf_path, sections):
if not location or "?" in location:
continue
if IS_WINDOWS:
drive, tail = splitdrive(location)
location = join(drive.upper(), tail)
drive, tail = os.path.splitdrive(location)
location = os.path.join(drive.upper(), tail)
symbol["file"] = location
symbol["line"] = 0
if ":" in location:
Expand All @@ -156,7 +156,7 @@ def _collect_symbols_info(env, elffile, elf_path, sections):
return symbols


def pioSizeDetermineSection(_, sections, symbol_addr):
def memusageDetermineSection(_, sections, symbol_addr):
for section, info in sections.items():
if not info.get("in_flash", False) and not info.get("in_ram", False):
continue
Expand All @@ -165,22 +165,22 @@ def pioSizeDetermineSection(_, sections, symbol_addr):
return "unknown"


def pioSizeIsValidSymbol(_, symbol_name, symbol_type, symbol_address):
def memusageIsValidSymbol(_, symbol_name, symbol_type, symbol_address):
return symbol_name and symbol_address != 0 and symbol_type != "STT_NOTYPE"


def pioSizeIsRamSection(_, section):
def memusageIsRamSection(_, section):
return (
section.get("type", "") in ("SHT_NOBITS", "SHT_PROGBITS")
and section.get("flags", "") == "WA"
)


def pioSizeIsFlashSection(_, section):
def memusageIsFlashSection(_, section):
return section.get("type", "") == "SHT_PROGBITS" and "A" in section.get("flags", "")


def pioSizeCalculateFirmwareSize(_, sections):
def memusageCalculateFirmwareSize(_, sections):
flash_size = ram_size = 0
for section_info in sections.values():
if section_info.get("in_flash", False):
Expand All @@ -191,8 +191,8 @@ def pioSizeCalculateFirmwareSize(_, sections):
return ram_size, flash_size


def DumpSizeData(_, target, source, env): # pylint: disable=unused-argument
data = {"device": {}, "memory": {}, "version": 1}
def DumpMemoryUsage(_, target, source, env): # pylint: disable=unused-argument
data = {"version": 1, "timestamp": int(time.time()), "device": {}, "memory": {}}

board = env.BoardConfig()
if board:
Expand All @@ -216,7 +216,7 @@ def DumpSizeData(_, target, source, env): # pylint: disable=unused-argument
env.Exit(1)

sections = _collect_sections_info(env, elffile)
firmware_ram, firmware_flash = env.pioSizeCalculateFirmwareSize(sections)
firmware_ram, firmware_flash = env.memusageCalculateFirmwareSize(sections)
data["memory"]["total"] = {
"ram_size": firmware_ram,
"flash_size": firmware_flash,
Expand All @@ -225,7 +225,7 @@ def DumpSizeData(_, target, source, env): # pylint: disable=unused-argument

files = {}
for symbol in _collect_symbols_info(env, elffile, elf_path, sections):
file_path = symbol.get("file") or "unknown"
file_path = symbol.pop("file", "unknown")
if not files.get(file_path, {}):
files[file_path] = {"symbols": [], "ram_size": 0, "flash_size": 0}

Expand All @@ -246,21 +246,21 @@ def DumpSizeData(_, target, source, env): # pylint: disable=unused-argument
file_data.update(v)
data["memory"]["files"].append(file_data)

with open(
join(env.subst("$BUILD_DIR"), "sizedata.json"), mode="w", encoding="utf8"
) as fp:
fp.write(json.dumps(data))
print(
"Memory usage report has been saved to the following location: "
f"\"{save_report(os.getcwd(), env['PIOENV'], data)}\""
)


def exists(_):
return True


def generate(env):
env.AddMethod(pioSizeIsRamSection)
env.AddMethod(pioSizeIsFlashSection)
env.AddMethod(pioSizeCalculateFirmwareSize)
env.AddMethod(pioSizeDetermineSection)
env.AddMethod(pioSizeIsValidSymbol)
env.AddMethod(DumpSizeData)
env.AddMethod(memusageIsRamSection)
env.AddMethod(memusageIsFlashSection)
env.AddMethod(memusageCalculateFirmwareSize)
env.AddMethod(memusageDetermineSection)
env.AddMethod(memusageIsValidSymbol)
env.AddMethod(DumpMemoryUsage)
return env
100 changes: 100 additions & 0 deletions platformio/home/rpc/handlers/memusage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright (c) 2014-present PlatformIO <[email protected]>
#
# 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.

import functools

from platformio.home.rpc.handlers.base import BaseRPCHandler
from platformio.project import memusage


class MemUsageRPC(BaseRPCHandler):
NAMESPACE = "memusage"

async def summary(self, project_dir, env, options=None):
options = options or {}
existing_reports = memusage.list_reports(project_dir, env)
current_report = previous_report = None
if options.get("cached") and existing_reports:
current_report = memusage.read_report(existing_reports[-1])
if len(existing_reports) > 1:
previous_report = memusage.read_report(existing_reports[-2])
else:
if existing_reports:
previous_report = memusage.read_report(existing_reports[-1])
await self.factory.manager.dispatcher["core.exec"](
["run", "-d", project_dir, "-e", env, "-t", "__memusage"],
options=options.get("exec"),
raise_exception=True,
)
current_report = memusage.read_report(
memusage.list_reports(project_dir, env)[-1]
)

max_top_items = 10
return dict(
timestamp=dict(
current=current_report["timestamp"],
previous=previous_report["timestamp"] if previous_report else None,
),
device=current_report["device"],
trend=dict(
current=current_report["memory"]["total"],
previous=previous_report["memory"]["total"]
if previous_report
else None,
),
top=dict(
files=self._calculate_top_files(current_report["memory"]["files"])[
0:max_top_items
],
symbols=self._calculate_top_symbols(current_report["memory"]["files"])[
0:max_top_items
],
sections=sorted(
current_report["memory"]["total"]["sections"].values(),
key=lambda item: item["size"],
reverse=True,
)[0:max_top_items],
),
)

@staticmethod
def _calculate_top_files(items):
return [
{"path": item["path"], "ram": item["ram_size"], "flash": item["flash_size"]}
for item in sorted(
items,
key=lambda item: item["ram_size"] + item["flash_size"],
reverse=True,
)
]

@staticmethod
def _calculate_top_symbols(files):
symbols = functools.reduce(
lambda result, filex: result
+ [
{
"name": s["name"],
"type": s["type"],
"size": s["size"],
"file": filex["path"],
"line": s.get("line"),
}
for s in filex["symbols"]
],
files,
[],
)
return sorted(symbols, key=lambda item: item["size"], reverse=True)
2 changes: 2 additions & 0 deletions platformio/home/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from platformio.home.rpc.handlers.app import AppRPC
from platformio.home.rpc.handlers.core import CoreRPC
from platformio.home.rpc.handlers.ide import IDERPC
from platformio.home.rpc.handlers.memusage import MemUsageRPC
from platformio.home.rpc.handlers.misc import MiscRPC
from platformio.home.rpc.handlers.os import OSRPC
from platformio.home.rpc.handlers.platform import PlatformRPC
Expand Down Expand Up @@ -70,6 +71,7 @@ def run_server(host, port, no_open, shutdown_timeout, home_url):
ws_rpc_factory.add_object_handler(AccountRPC())
ws_rpc_factory.add_object_handler(AppRPC())
ws_rpc_factory.add_object_handler(IDERPC())
ws_rpc_factory.add_object_handler(MemUsageRPC())
ws_rpc_factory.add_object_handler(MiscRPC())
ws_rpc_factory.add_object_handler(OSRPC())
ws_rpc_factory.add_object_handler(CoreRPC())
Expand Down
3 changes: 1 addition & 2 deletions platformio/project/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ def get_build_type(config, env, run_targets=None):
run_targets = run_targets or []
declared_build_type = config.get(f"env:{env}", "build_type")
if (
set(["__debug", "sizedata"]) # sizedata = for memory inspection
& set(run_targets)
set(["__debug", "__memusage"]) & set(run_targets)
or declared_build_type == "debug"
):
types.append("debug")
Expand Down
Loading

0 comments on commit a3ad310

Please sign in to comment.