Compare commits

...

45 Commits

Author SHA1 Message Date
Christopher Haster
40dba4a556 Merge pull request #669 from littlefs-project/devel
Minor release: v2.5
2022-04-13 22:49:41 -05:00
Christopher Haster
148e312ea3 Bumped minor version to v2.5 2022-04-13 22:47:43 -05:00
Christopher Haster
abbfe8e92e Reduced lfs_dir_traverse's explicit stack to 3 frames
This is possible thanks to invoxiaamo's optimization of compacting
renames to avoid the O(n^3) nested filters. Not only does this
significantly reduce the runtime cost of that operation, but it
reduces the maximum possible depth of recursion to 3 frames.

Deepest lfs_dir_traverse before:

traverse with commit
'-> traverse with filter
    '-> traverse with move
        '-> traverse with filter

Deepest lfs_dir_traverse after:

traverse with commit
'-> traverse with move
    '-> traverse with filter
2022-04-10 23:27:49 -05:00
Christopher Haster
c60c977c25 Merge pull request #658 from littlefs-project/no-recursion
Restructure littlefs to not use recursion, measure stack usage
2022-04-10 23:23:39 -05:00
Christopher Haster
3ce64d1ac0 Merge pull request #666 from invoxiaamo/rename-opti2
Optimization of the rename case.
2022-04-10 22:02:04 -05:00
Christopher Haster
0ced3623d4 Merge pull request #657 from littlefs-project/copyright-update
Update copyright notice
2022-04-10 21:59:27 -05:00
Christopher Haster
5451a6d503 Merge pull request #643 from microist/fix-filebd-windows
Fixes to use lfs_filebd on windows platforms
2022-04-10 21:56:08 -05:00
Martin Hoffmann
1e038c81fc Fixes to use lfs_filebd on windows platforms
There are two issues, when using the file-based block device emulation
on Windows Platforms:
1. There is no fsync implementation available. This needs to be mapped
   to a Windows-specific FlushFileBuffers system call.
2. The block device file needs to be opened as binary file (O_BINARY)
	   The corresponding flag is not required for Linux.
2022-04-10 21:55:00 -05:00
Christopher Haster
f28ac3ea7d Merge pull request #638 from lmapii/master
Removed invalid overwrite for return value.
2022-04-10 21:52:48 -05:00
Christopher Haster
a94fbda1cd Merge pull request #632 from robekras/patch-1
Fix lfs_file_rawseek performance issue
2022-04-10 21:52:27 -05:00
Christopher Haster
cc025653ed Merge pull request #630 from Johnxjj/dev-johnxjj
add the limit, the cursor cannot be set to a negative number
2022-04-10 14:44:47 -05:00
Christopher Haster
bfb9bd2483 Merge pull request #614 from nnayo/fix_no_malloc_2
don't use lfs_file_open() when LFS_NO_MALLOC is set
2022-04-10 14:44:33 -05:00
Christopher Haster
f40b854ab5 Merge pull request #584 from colin-foster-in-advantage/block_size_mount_fail
Fail mount when the block size changes
2022-04-10 14:44:24 -05:00
Arnaud Mouiche
c2fa1bb7df Optimization of the rename case.
Rename can be VERY time consuming. One of the reasons is the 4 recursion
level depth of lfs_dir_traverse() seen if a compaction happened during the
rename.

lfs_dir_compact()
  size computation
    [1] lfs_dir_traverse(cb=lfs_dir_commit_size)
         - do 'duplicates and tag update'
       [2] lfs_dir_traverse(cb=lfs_dir_traverse_filter, data=tag[1])
           - Reaching a LFS_FROM_MOVE tag (here)
         [3] lfs_dir_traverse(cb=lfs_dir_traverse_filter, data=tag[1]) <= on 'from' dir
             - do 'duplicates and tag update'
           [4] lfs_dir_traverse(cb=lfs_dir_traverse_filter, data=tag[3])
  followed by the compaction itself:
    [1] lfs_dir_traverse(cb=lfs_dir_commit_commit)
         - do 'duplicates and tag update'
       [2] lfs_dir_traverse(cb=lfs_dir_traverse_filter, data=tag[1])
           - Reaching a LFS_FROM_MOVE tag (here)
         [3] lfs_dir_traverse(cb=lfs_dir_traverse_filter, data=tag[1]) <= on 'from' dir
             - do 'duplicates and tag update'
           [4] lfs_dir_traverse(cb=lfs_dir_traverse_filter, data=tag[3])

Yet, analyse shows that levels [3] and [4] don't perform anything
if the callback is lfs_dir_traverse_filter...

A practical example:

- format and mount a 4KB block FS
- create 100 files of 256 Bytes named "/dummy_%d"
- create a 1024 Byte file "/test"
- rename "/test" "/test_rename"
- create a 1024 Byte file "/test"
- rename "/test" "/test_rename"
This triggers a compaction where lfs_dir_traverse was called 148393 times,
generating 25e6+ lfs_bd_read calls (~100 MB+ of data)

With the optimization, lfs_dir_traverse is now called 3248 times
(589e3 lfs_bds_calls (~2.3MB of data)

=> x 43 improvement...
2022-04-10 13:12:45 -05:00
martin
3b62ec1c47 Updated error handling for NOSPC 2022-04-10 13:00:13 -05:00
xujunjun
b898977fd8 Set the limit, the cursor cannot be set to a negative number 2022-04-10 12:57:42 -05:00
Colin Foster
cf274e6ec6 Squash of CR changes
- nit: Moving brace to end of if statement line for consistency
- mount: add more debug info per CR
- Fix compiler error from extra parentheses
- Fix superblock typo
2022-04-10 12:53:33 -05:00
Christopher Haster
425dc810a5 Modified robekras's optimization to avoid flush for all seeks in cache
The basic idea is simple, if we seek to a position in the currently
loaded cache, don't flush the cache. Notably this ensures that seek is
always as fast or faster than just reading the data.

This is a bit tricky since we need to check that our new block and
offset match the cache, fortunately we can skip the block check by
reevaluating the block index for both the current and new positions.

Note this only works whene reading, for writing we need to always flush
the cache, or else we will lose the pending write data.
2022-04-10 12:46:51 -05:00
robekras
a6f01b7d6e Update lfs.c
This should fix the performance issue if a new seek position belongs to currently cached data.
This avoids unnecessary rereads of file data.
2022-04-09 02:12:18 -05:00
Christopher Haster
2db5dc80c2 Update copyright notice 2022-03-20 23:03:52 -05:00
Christopher Haster
8109f28266 Removed recursion from lfs_dir_traverse
lfs_dir_traverse is a bit unpleasant in that it is inherently a
recursive function, but without a strict bound of 4 calls (commit -> filter ->
move -> filter), and efforts to unroll the recursion comes at a
signification code cost.

It turns out the best solution I've found so far is to simple create an
explicit stack with an explicit bound of 4 calls (or more accurately,
3 pushed frames).

---

This actually highlights one of the bigger flaws in littlefs right now,
which is that this function, lfs_dir_traverse, takes O(n^2) disk reads
to traverse.

Note that LFS_FROM_MOVE can only occur once per commit, which is why
this code is O(n^2) and not O(n^4).
2022-03-20 04:27:54 -05:00
Christopher Haster
fedf646c79 Removed recursion in file read/writes
This mostly just required separate functions for "lfs_file_rawwrite" and
"lfs_file_flushedwrite", since lfs_file_flush recursively invokes
lfs_file_rawread and lfs_file_rawwrite.

This comes at a code cost, but gives us bounded and measurable RAM usage
on this code path.
2022-03-20 04:25:24 -05:00
Christopher Haster
84da4c0b1a Removed recursion from commit/relocate code path
lfs_dir_commit originally relied heavily on tail-recursion, though at
least one path (through relocations) was not tail-recursive, and could
cause unbounded stack usage in extreme cases of bad blocks. (Keep in
mind even extreme cases of bad blocks should be in scope for littlefs).

In order to remove recursion from this code path, several changed were
raequired:

- The lfs_dir_compact logic had to be somewhat inverted. Instead of
  first compacting and then resolving issues such as relocations and
  orphans, the overarching lfs_dir_commit now contains a state-machine
  which after committing or compacting handles the extra changes to the
  filesystem in a single, non-recursive loop

- Instead of fixing all relocations recursively, >1 relocation requires
  defering to a full deorphan step. This step is unfortunately an
  additional n^2 process. It also required some changes to lfs_deorphan
  in order to ignore intentional orphans created as an intermediary in
  lfs_mkdir. Maybe in the future we should remove these.

- Tail recursion normally found in lfs_fs_deorphan had to be rewritten
  as a loop which restarts any time a new commit causes a relocation.
  This does show that the algorithm may not terminate, but only if every
  block is bad, which will eventually cause littlefs to run out of
  blocks to write to.
2022-03-20 04:24:44 -05:00
Christopher Haster
554e4b1444 Fixed Popen deadlock issue in test.py
As noted in Python's subprocess library:

> This will deadlock when using stdout=PIPE and/or stderr=PIPE and the
> child process generates enough output to a pipe such that it blocks
> waiting for the OS pipe buffer to accept more data.

Curiously, this only became a problem when updating to Ubuntu 20.04
in CI (python3.6 -> python3.8).
2022-03-20 03:44:39 -05:00
Christopher Haster
fe8f3d4f18 Changed./scripts/struct.py to organize by header file
Avoids redundant counting of structs shared in multiple .c files, which
is very common. This is different from the other scripts,
code.py/data.py/stack.py, but this difference makes sense as struct
declarations have a very different lifetime.
2022-03-20 03:41:37 -05:00
Christopher Haster
316b019f41 In CI, determine loop devices dynamically to avoid conflicts with Ubuntu snaps
Introduced when updating CI to Ubuntu 20.04, Ubuntu snaps consume
loop devices, which conflict with out assumption that /dev/loop0
will always be unused. Changed to request a dynamic loop device from
losetup, though it would have been nice if Ubuntu snaps allocated
from the last device or something.
2022-03-20 03:39:23 -05:00
Christopher Haster
8475c8064d Limit ./scripts/structs.py to report structs in local .h files
This requires parsing an additional section of the dwarfinfo (--dwarf=rawlines)
to get the declaration file info.

---

Interpreting the results of ./scripts/structs.py reporting is a bit more
complicated than other scripts, structs aren't used in a consistent
manner so the cost of a large struct depends on the context in which it
is used.

But that being said, there really isn't much reason to report
internal-only structs. These structs really only exist for type-checking
in internal algorithms, and their cost will end up reflected in other RAM
measurements, either stack, heap, or other.
2022-03-20 03:39:23 -05:00
Christopher Haster
563af5f364 Cleaned up make clean 2022-03-20 03:39:23 -05:00
Christopher Haster
3b495bab79 Fixed spurious CI failure caused by multiple writers to .o files
GCC is a bit frustrating here, it really wants to generate every file in
a single command, which _is_ more efficient if our build system could
leverage this. But -fcallgraph-info is a rather novel flag, so we can't
really rely on it for generally compiling and testing littlefs.

The multi-file output gets in the way when we want an explicitly
separate rule for callgraph-info generation. We can't generate the
callgraph-info without generating the objects files.

This becomes a surprsing issue when parallel building (make -j) is used!
Suddenly we might end up with both the .o and .ci rules writing to .o
files, which creates a really difficult to track down issue of corrupted
.o files.

The temporary solution is to use an order-only prerequisite. This still
ends up building the .o files twice, but it's an acceptable tradeoff for
not requiring the -fcallgraph-info for all builds.
2022-03-20 03:39:18 -05:00
Christopher Haster
e4adefd1d7 Fixed spurious encoding error
Using errors=replace in python utf-8 decoding makes these scripts more
resilient to underlying errors, rather than just throwing an unhelpfully
generic decode error.
2022-03-20 03:28:26 -05:00
Christopher Haster
9d54603ce2 Added new scripts to CI results
- Added to GitHub statuses (61 results)

- Reworked generated release table to include these (16 results, only thumb)

These also required a surprisingly large number of other changes:

- Bumbed CI Ubuntu version 18.04 -> 20.04, 22.04 is already on the
  horizon but not usable in GitHub yet

- Manualy upgrade to GCC v10, this is required for the -fcallgraph-info
  flag that scripts/stack.py uses.

- Increased paginated status queries to 100 per-page. If we have more
  statuses than this the status diffs may get much more complicated...

- Forced whitespace in generated release table to always be nbsp. GitHub
  tables get scrunched rather ugly without this, prefering margins to
  readable tables.

- Added limited support for "∞" results, since this is returned by
  ./scripts/stack.py for recursive functions.

As a side-note, this increases the number of statuses reported
per-commit from 6 to 61, so hopefully that doesn't cause any problems...
2022-03-20 03:28:26 -05:00
Christopher Haster
7ea2b515aa A few more tweaks to scripts
- Changed `make summary` to show a one line summary
- Added `make lfs.csv` rule, which is useful for finding more info with
  other scripts
- Fixed small issue in ./scripts/summary.py
- Added *.ci (callgraph) and *.csv (script output) to CI
2022-03-20 03:28:26 -05:00
Christopher Haster
55b3c538d5 Added ./script/summary.py
A full summary of static measurements (code size, stack usage, etc) can now
be found with:

    make summary

This is done through the combination of a new ./scripts/summary.py
script and the ability of existing scripts to merge into existing csv
files, allowing multiple results to be merged either in a pipeline, or
in parallel with a single ./script/summary.py call.

The ./scripts/summary.py script can also be used to quickly compare
different builds or configurations. This is a proper implementation
of a similar but hacky shell script that has already been very useful
for making optimization decisions:

    $ ./scripts/structs.py new.csv -d old.csv --summary
    name (2 added, 0 removed)               code             stack            structs
    TOTAL                                  28648 (-2.7%)      2448               1012

Also some other small tweaks to scripts:

- Removed state saving diff rules. This isn't the most useful way to
  handle comparing changes.

- Added short flags for --summary (-Y) and --files (-F), since these
  are quite often used.
2022-03-20 03:28:26 -05:00
Christopher Haster
eb8be9f351 Some improvements to size scripts
- Added -L/--depth argument to show dependencies for scripts/stack.py,
  this replaces calls.py
- Additional internal restructuring to avoid repeated code
- Removed incorrect diff percentage when there is no actual size
- Consistent percentage rendering in test.py
2022-03-20 03:28:21 -05:00
Christopher Haster
50ad2adc96 Added make *-diff rules, quick commands to compare sizes
This required a patch to the --diff flag for the scripts to ignore
a missing file. This enables the useful one liner for making comparisons
with potentially missing previous versions:

    ./scripts/code.py lfs.o -d lfs.o.code.csv -o lfs.o.code.csv

    function (0 added, 0 removed)            old     new    diff
    TOTAL                                  25476   25476      +0

One downside, these previous files are easy to delete as a part of make
clean, which limits their usefulness for comparing configuration
changes...
2022-03-11 14:40:54 -06:00
Christopher Haster
0a2ff3b6ff Added scripts/structs.py for getting sizes of structs
Note this does include internal structs, so this should probably
be limited to informative purposes.
2022-03-11 14:40:54 -06:00
Christopher Haster
d7582efec8 Changed script's CSV formats to allow for merging different measurements
- size  -> code_size
- size  -> data_size
- frame -> stack_frame
- limit -> stack_limit
- hits  -> coverage_hits
- count -> coverage_count
2022-03-11 14:40:54 -06:00
Christopher Haster
f4c7af76f8 Added scripts/stack.py for viewing stack usage
Note this detects loops (recursion), and renders this as infinity.
Currently littlefs does have a single recursive function and you can see
how this infects the full call graph. Eventually this should be removed.
2022-03-11 14:40:54 -06:00
Christopher Haster
20c58dcbaa Added coverage-sort to scripts/coverage.py
scripts/coverage.py was missed originally because it's not ran as often
as the others. Since it requires run-time info, it's usually only used
in CI.
2022-03-11 14:39:38 -06:00
Christopher Haster
f5286abe7a Added scripts/calls.py for viewing the callgraph directly 2022-03-11 14:39:36 -06:00
Christopher Haster
2cdabe810d Split out scripts/code.py into scripts/code.py and scripts/data.py
This is to avoid unexpected script behavior even though data.py should
always return 0 bytes for littlefs. Maybe a check for this should be
added to CI?
2022-03-11 14:39:36 -06:00
Christopher Haster
b045436c23 Added size-sort options to scripts/code.py
Now with -s/--sort and -S/--reverse-sort for sorting the functions by
size.

You may wonder why add reverse-sort, since its utility doesn't seem
worth the cost to implement (these are just helper scripts after all),
the reason is that reverse-sort is quite useful on the command-line,
where scrollback may be truncated, and you only care about the larger
entries.

Outside of the command-line, normal sort is prefered.

Fortunately the difference is just the sign in the sort key.

Note this conflicts with the short --summary flag, so that has been
removed.
2022-03-11 14:36:23 -06:00
yog
e334983767 don't use lfs_file_open() when LFS_NO_MALLOC is set 2022-02-18 20:57:20 -06:00
Colin Foster
487df12dde Fail when block_size doesn't match config
With the previous commit, fail if the superblock block_size doesn't
match the config block_size.
2021-08-17 10:02:27 -07:00
Colin Foster
3efb8e44f3 Fail mount when the block size changes
When the on-disk block size doesn't match the config block size, it is
possible to get file corruption. For instance, if the num blocks was
0x200 and we re-mount with 0x100 files could be corrupt.

If we re-mount with a larger number of blocks things should be safer,
but could be handled with a resize option or perhaps a mount flag to
ignore this parameter.
2021-07-21 08:56:21 -07:00
24 changed files with 2669 additions and 727 deletions

View File

@@ -6,7 +6,7 @@ on:
jobs: jobs:
post-release: post-release:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
steps: steps:
# trigger post-release in dependency repo, this indirection allows the # trigger post-release in dependency repo, this indirection allows the
# dependency repo to be updated often without affecting this repo. At # dependency repo to be updated often without affecting this repo. At

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
# need to manually check for a couple things # need to manually check for a couple things
# - tests passed? # - tests passed?
@@ -73,89 +73,70 @@ jobs:
# previous results to compare against? # previous results to compare against?
[ -n "$LFS_PREV_VERSION" ] && curl -sS \ [ -n "$LFS_PREV_VERSION" ] && curl -sS \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/` "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/`
`status/$LFS_PREV_VERSION" \ `status/$LFS_PREV_VERSION?per_page=100" \
| jq -re 'select(.sha != env.GITHUB_SHA) | .statuses[]' \ | jq -re 'select(.sha != env.GITHUB_SHA) | .statuses[]' \
>> prev-results.json \ >> prev-results.json \
|| true || true
# unfortunately these each have their own format # build table for GitHub
[ -e results/code-thumb.csv ] && ( \ echo "<table>" >> results.txt
export PREV="$(jq -re ' echo "<thead>" >> results.txt
select(.context == "results / code").description echo "<tr>" >> results.txt
| capture("Code size is (?<result>[0-9]+)").result' \ echo "<th align=left>Configuration</th>" >> results.txt
prev-results.json || echo 0)" for r in Code Stack Structs Coverage
./scripts/code.py -u results/code-thumb.csv -s | awk ' do
NR==2 {printf "Code size,%d B",$2} echo "<th align=right>$r</th>" >> results.txt
NR==2 && ENVIRON["PREV"]+0 != 0 { done
printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]} echo "</tr>" >> results.txt
NR==2 {printf "\n"}' \ echo "</thead>" >> results.txt
>> results.csv)
[ -e results/code-thumb-readonly.csv ] && ( \ echo "<tbody>" >> results.txt
export PREV="$(jq -re ' for c in "" readonly threadsafe migrate error-asserts
select(.context == "results / code (readonly)").description do
| capture("Code size is (?<result>[0-9]+)").result' \ echo "<tr>" >> results.txt
prev-results.json || echo 0)" c_or_default=${c:-default}
./scripts/code.py -u results/code-thumb-readonly.csv -s | awk ' echo "<td align=left>${c_or_default^}</td>" >> results.txt
NR==2 {printf "Code size<br/>(readonly),%d B",$2} for r in code stack structs
NR==2 && ENVIRON["PREV"]+0 != 0 { do
printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]} # per-config results
NR==2 {printf "\n"}' \ echo "<td align=right>" >> results.txt
>> results.csv) [ -e results/thumb${c:+-$c}.csv ] && ( \
[ -e results/code-thumb-threadsafe.csv ] && ( \ export PREV="$(jq -re '
export PREV="$(jq -re ' select(.context == "'"results (thumb${c:+, $c}) / $r"'").description
select(.context == "results / code (threadsafe)").description | capture("(?<result>[0-9∞]+)").result' \
| capture("Code size is (?<result>[0-9]+)").result' \ prev-results.json || echo 0)"
prev-results.json || echo 0)" ./scripts/summary.py results/thumb${c:+-$c}.csv -f $r -Y | awk '
./scripts/code.py -u results/code-thumb-threadsafe.csv -s | awk ' NR==2 {printf "%s B",$2}
NR==2 {printf "Code size<br/>(threadsafe),%d B",$2} NR==2 && ENVIRON["PREV"]+0 != 0 {
NR==2 && ENVIRON["PREV"]+0 != 0 { printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}
printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]} NR==2 {printf "\n"}' \
NR==2 {printf "\n"}' \ | sed -e 's/ /\&nbsp;/g' \
>> results.csv) >> results.txt)
[ -e results/code-thumb-migrate.csv ] && ( \ echo "</td>" >> results.txt
export PREV="$(jq -re ' done
select(.context == "results / code (migrate)").description # coverage results
| capture("Code size is (?<result>[0-9]+)").result' \ if [ -z $c ]
prev-results.json || echo 0)" then
./scripts/code.py -u results/code-thumb-migrate.csv -s | awk ' echo "<td rowspan=0 align=right>" >> results.txt
NR==2 {printf "Code size<br/>(migrate),%d B",$2} [ -e results/coverage.csv ] && ( \
NR==2 && ENVIRON["PREV"]+0 != 0 { export PREV="$(jq -re '
printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]} select(.context == "results / coverage").description
NR==2 {printf "\n"}' \ | capture("(?<result>[0-9\\.]+)").result' \
>> results.csv) prev-results.json || echo 0)"
[ -e results/code-thumb-error-asserts.csv ] && ( \ ./scripts/coverage.py -u results/coverage.csv -Y | awk -F '[ /%]+' '
export PREV="$(jq -re ' NR==2 {printf "%.1f%% of %d lines",$4,$3}
select(.context == "results / code (error-asserts)").description NR==2 && ENVIRON["PREV"]+0 != 0 {
| capture("Code size is (?<result>[0-9]+)").result' \ printf " (%+.1f%%)",$4-ENVIRON["PREV"]}
prev-results.json || echo 0)" NR==2 {printf "\n"}' \
./scripts/code.py -u results/code-thumb-error-asserts.csv -s | awk ' | sed -e 's/ /\&nbsp;/g' \
NR==2 {printf "Code size<br/>(error-asserts),%d B",$2} >> results.txt)
NR==2 && ENVIRON["PREV"]+0 != 0 { echo "</td>" >> results.txt
printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]} fi
NR==2 {printf "\n"}' \ echo "</tr>" >> results.txt
>> results.csv) done
[ -e results/coverage.csv ] && ( \ echo "</tbody>" >> results.txt
export PREV="$(jq -re ' echo "</table>" >> results.txt
select(.context == "results / coverage").description
| capture("Coverage is (?<result>[0-9\\.]+)").result' \
prev-results.json || echo 0)"
./scripts/coverage.py -u results/coverage.csv -s | awk -F '[ /%]+' '
NR==2 {printf "Coverage,%.1f%% of %d lines",$4,$3}
NR==2 && ENVIRON["PREV"]+0 != 0 {
printf " (%+.1f%%)",$4-ENVIRON["PREV"]}
NR==2 {printf "\n"}' \
>> results.csv)
# transpose to GitHub table
[ -e results.csv ] || exit 0
awk -F ',' '
{label[NR]=$1; value[NR]=$2}
END {
for (r=1; r<=NR; r++) {printf "| %s ",label[r]}; printf "|\n";
for (r=1; r<=NR; r++) {printf "|:--"}; printf "|\n";
for (r=1; r<=NR; r++) {printf "| %s ",value[r]}; printf "|\n"}' \
results.csv > results.txt
echo "RESULTS:"
cat results.txt cat results.txt
# find changes from history # find changes from history

View File

@@ -6,7 +6,7 @@ on:
jobs: jobs:
status: status:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
steps: steps:
# custom statuses? # custom statuses?
- uses: dawidd6/action-download-artifact@v2 - uses: dawidd6/action-download-artifact@v2

View File

@@ -8,7 +8,7 @@ env:
jobs: jobs:
# run tests # run tests
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -18,11 +18,27 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: install - name: install
run: | run: |
# need toml, also pip3 isn't installed by default? # need a few additional tools
#
# note this includes gcc-10, which is required for -fcallgraph-info=su
sudo apt-get update -qq sudo apt-get update -qq
sudo apt-get install -qq python3 python3-pip lcov sudo apt-get install -qq gcc-10 python3 python3-pip lcov
sudo pip3 install toml sudo pip3 install toml
gcc --version echo "CC=gcc-10" >> $GITHUB_ENV
gcc-10 --version
lcov --version
python3 --version
# need newer lcov version for gcc-10
#sudo apt-get remove lcov
#wget https://launchpad.net/ubuntu/+archive/primary/+files/lcov_1.15-1_all.deb
#sudo apt install ./lcov_1.15-1_all.deb
#lcov --version
#which lcov
#ls -lha /usr/bin/lcov
wget https://github.com/linux-test-project/lcov/releases/download/v1.15/lcov-1.15.tar.gz
tar xf lcov-1.15.tar.gz
sudo make -C lcov-1.15 install
# setup a ram-backed disk to speed up reentrant tests # setup a ram-backed disk to speed up reentrant tests
mkdir disks mkdir disks
@@ -41,36 +57,36 @@ jobs:
if: ${{matrix.arch == 'thumb'}} if: ${{matrix.arch == 'thumb'}}
run: | run: |
sudo apt-get install -qq \ sudo apt-get install -qq \
gcc-arm-linux-gnueabi \ gcc-10-arm-linux-gnueabi \
libc6-dev-armel-cross \ libc6-dev-armel-cross \
qemu-user qemu-user
echo "CC=arm-linux-gnueabi-gcc -mthumb --static" >> $GITHUB_ENV echo "CC=arm-linux-gnueabi-gcc-10 -mthumb --static" >> $GITHUB_ENV
echo "EXEC=qemu-arm" >> $GITHUB_ENV echo "EXEC=qemu-arm" >> $GITHUB_ENV
arm-linux-gnueabi-gcc --version arm-linux-gnueabi-gcc-10 --version
qemu-arm -version qemu-arm -version
# cross-compile with MIPS (32-bit, big-endian) # cross-compile with MIPS (32-bit, big-endian)
- name: install-mips - name: install-mips
if: ${{matrix.arch == 'mips'}} if: ${{matrix.arch == 'mips'}}
run: | run: |
sudo apt-get install -qq \ sudo apt-get install -qq \
gcc-mips-linux-gnu \ gcc-10-mips-linux-gnu \
libc6-dev-mips-cross \ libc6-dev-mips-cross \
qemu-user qemu-user
echo "CC=mips-linux-gnu-gcc --static" >> $GITHUB_ENV echo "CC=mips-linux-gnu-gcc-10 --static" >> $GITHUB_ENV
echo "EXEC=qemu-mips" >> $GITHUB_ENV echo "EXEC=qemu-mips" >> $GITHUB_ENV
mips-linux-gnu-gcc --version mips-linux-gnu-gcc-10 --version
qemu-mips -version qemu-mips -version
# cross-compile with PowerPC (32-bit, big-endian) # cross-compile with PowerPC (32-bit, big-endian)
- name: install-powerpc - name: install-powerpc
if: ${{matrix.arch == 'powerpc'}} if: ${{matrix.arch == 'powerpc'}}
run: | run: |
sudo apt-get install -qq \ sudo apt-get install -qq \
gcc-powerpc-linux-gnu \ gcc-10-powerpc-linux-gnu \
libc6-dev-powerpc-cross \ libc6-dev-powerpc-cross \
qemu-user qemu-user
echo "CC=powerpc-linux-gnu-gcc --static" >> $GITHUB_ENV echo "CC=powerpc-linux-gnu-gcc-10 --static" >> $GITHUB_ENV
echo "EXEC=qemu-ppc" >> $GITHUB_ENV echo "EXEC=qemu-ppc" >> $GITHUB_ENV
powerpc-linux-gnu-gcc --version powerpc-linux-gnu-gcc-10 --version
qemu-ppc -version qemu-ppc -version
# make sure example can at least compile # make sure example can at least compile
@@ -148,102 +164,108 @@ jobs:
retention-days: 1 retention-days: 1
# update results # update results
- name: results-code - name: results
run: | run: |
mkdir -p results mkdir -p results
make clean make clean
make code \ make lfs.csv \
CFLAGS+=" \ CFLAGS+=" \
-DLFS_NO_ASSERT \ -DLFS_NO_ASSERT \
-DLFS_NO_DEBUG \ -DLFS_NO_DEBUG \
-DLFS_NO_WARN \ -DLFS_NO_WARN \
-DLFS_NO_ERROR" \ -DLFS_NO_ERROR"
CODEFLAGS+="-o results/code-${{matrix.arch}}.csv" cp lfs.csv results/${{matrix.arch}}.csv
- name: results-code-readonly ./scripts/summary.py results/${{matrix.arch}}.csv
- name: results-readonly
run: | run: |
mkdir -p results mkdir -p results
make clean make clean
make code \ make lfs.csv \
CFLAGS+=" \ CFLAGS+=" \
-DLFS_NO_ASSERT \ -DLFS_NO_ASSERT \
-DLFS_NO_DEBUG \ -DLFS_NO_DEBUG \
-DLFS_NO_WARN \ -DLFS_NO_WARN \
-DLFS_NO_ERROR \ -DLFS_NO_ERROR \
-DLFS_READONLY" \ -DLFS_READONLY"
CODEFLAGS+="-o results/code-${{matrix.arch}}-readonly.csv" cp lfs.csv results/${{matrix.arch}}-readonly.csv
- name: results-code-threadsafe ./scripts/summary.py results/${{matrix.arch}}-readonly.csv
- name: results-threadsafe
run: | run: |
mkdir -p results mkdir -p results
make clean make clean
make code \ make lfs.csv \
CFLAGS+=" \ CFLAGS+=" \
-DLFS_NO_ASSERT \ -DLFS_NO_ASSERT \
-DLFS_NO_DEBUG \ -DLFS_NO_DEBUG \
-DLFS_NO_WARN \ -DLFS_NO_WARN \
-DLFS_NO_ERROR \ -DLFS_NO_ERROR \
-DLFS_THREADSAFE" \ -DLFS_THREADSAFE"
CODEFLAGS+="-o results/code-${{matrix.arch}}-threadsafe.csv" cp lfs.csv results/${{matrix.arch}}-threadsafe.csv
- name: results-code-migrate ./scripts/summary.py results/${{matrix.arch}}-threadsafe.csv
- name: results-migrate
run: | run: |
mkdir -p results mkdir -p results
make clean make clean
make code \ make lfs.csv \
CFLAGS+=" \ CFLAGS+=" \
-DLFS_NO_ASSERT \ -DLFS_NO_ASSERT \
-DLFS_NO_DEBUG \ -DLFS_NO_DEBUG \
-DLFS_NO_WARN \ -DLFS_NO_WARN \
-DLFS_NO_ERROR \ -DLFS_NO_ERROR \
-DLFS_MIGRATE" \ -DLFS_MIGRATE"
CODEFLAGS+="-o results/code-${{matrix.arch}}-migrate.csv" cp lfs.csv results/${{matrix.arch}}-migrate.csv
- name: results-code-error-asserts ./scripts/summary.py results/${{matrix.arch}}-migrate.csv
- name: results-error-asserts
run: | run: |
mkdir -p results mkdir -p results
make clean make clean
make code \ make lfs.csv \
CFLAGS+=" \ CFLAGS+=" \
-DLFS_NO_DEBUG \ -DLFS_NO_DEBUG \
-DLFS_NO_WARN \ -DLFS_NO_WARN \
-DLFS_NO_ERROR \ -DLFS_NO_ERROR \
-D'LFS_ASSERT(test)=do {if(!(test)) {return -1;}} while(0)'" \ -D'LFS_ASSERT(test)=do {if(!(test)) {return -1;}} while(0)'"
CODEFLAGS+="-o results/code-${{matrix.arch}}-error-asserts.csv" cp lfs.csv results/${{matrix.arch}}-error-asserts.csv
./scripts/summary.py results/${{matrix.arch}}-error-asserts.csv
- name: upload-results - name: upload-results
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: results name: results
path: results path: results
# limit reporting to Thumb, otherwise there would be too many numbers
# flying around for the results to be easily readable # create statuses with results
- name: collect-status - name: collect-status
if: ${{matrix.arch == 'thumb'}}
run: | run: |
mkdir -p status mkdir -p status
for f in $(shopt -s nullglob ; echo results/code*.csv) for f in $(shopt -s nullglob ; echo results/*.csv)
do do
export STEP="results-code$( export STEP="results$(
echo $f | sed -n 's/.*code-.*-\(.*\).csv/-\1/p')" echo $f | sed -n 's/[^-]*-\(.*\).csv/-\1/p')"
export CONTEXT="results / code$( for r in code stack structs
echo $f | sed -n 's/.*code-.*-\(.*\).csv/ (\1)/p')" do
export PREV="$(curl -sS \ export CONTEXT="results (${{matrix.arch}}$(
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/status/master" \ echo $f | sed -n 's/[^-]*-\(.*\).csv/, \1/p')) / $r"
| jq -re 'select(.sha != env.GITHUB_SHA) | .statuses[] export PREV="$(curl -sS \
| select(.context == env.CONTEXT).description "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/status/master?per_page=100" \
| capture("Code size is (?<result>[0-9]+)").result' \ | jq -re 'select(.sha != env.GITHUB_SHA) | .statuses[]
|| echo 0)" | select(.context == env.CONTEXT).description
export DESCRIPTION="$(./scripts/code.py -u $f -s | awk ' | capture("(?<result>[0-9∞]+)").result' \
NR==2 {printf "Code size is %d B",$2} || echo 0)"
NR==2 && ENVIRON["PREV"]+0 != 0 { export DESCRIPTION="$(./scripts/summary.py $f -f $r -Y | awk '
printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}')" NR==2 {printf "%s B",$2}
jq -n '{ NR==2 && ENVIRON["PREV"]+0 != 0 {
state: "success", printf " (%+.1f%%)",100*($2-ENVIRON["PREV"])/ENVIRON["PREV"]}')"
context: env.CONTEXT, jq -n '{
description: env.DESCRIPTION, state: "success",
target_job: "${{github.job}} (${{matrix.arch}})", context: env.CONTEXT,
target_step: env.STEP}' \ description: env.DESCRIPTION,
| tee status/code$( target_job: "${{github.job}} (${{matrix.arch}})",
echo $f | sed -n 's/.*code-.*-\(.*\).csv/-\1/p').json target_step: env.STEP}' \
| tee status/$r-${{matrix.arch}}$(
echo $f | sed -n 's/[^-]*-\(.*\).csv/-\1/p').json
done
done done
- name: upload-status - name: upload-status
if: ${{matrix.arch == 'thumb'}}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: status name: status
@@ -252,7 +274,7 @@ jobs:
# run under Valgrind to check for memory errors # run under Valgrind to check for memory errors
valgrind: valgrind:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: install - name: install
@@ -272,7 +294,7 @@ jobs:
# self-host with littlefs-fuse for a fuzz-like test # self-host with littlefs-fuse for a fuzz-like test
fuse: fuse:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
if: ${{!endsWith(github.ref, '-prefix')}} if: ${{!endsWith(github.ref, '-prefix')}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -297,16 +319,18 @@ jobs:
# setup disk for littlefs-fuse # setup disk for littlefs-fuse
mkdir mount mkdir mount
sudo chmod a+rw /dev/loop0 LOOP=$(sudo losetup -f)
sudo chmod a+rw $LOOP
dd if=/dev/zero bs=512 count=128K of=disk dd if=/dev/zero bs=512 count=128K of=disk
losetup /dev/loop0 disk losetup $LOOP disk
echo "LOOP=$LOOP" >> $GITHUB_ENV
- name: test - name: test
run: | run: |
# self-host test # self-host test
make -C littlefs-fuse make -C littlefs-fuse
littlefs-fuse/lfs --format /dev/loop0 littlefs-fuse/lfs --format $LOOP
littlefs-fuse/lfs /dev/loop0 mount littlefs-fuse/lfs $LOOP mount
ls mount ls mount
mkdir mount/littlefs mkdir mount/littlefs
@@ -318,7 +342,7 @@ jobs:
# test migration using littlefs-fuse # test migration using littlefs-fuse
migrate: migrate:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
if: ${{!endsWith(github.ref, '-prefix')}} if: ${{!endsWith(github.ref, '-prefix')}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -348,9 +372,11 @@ jobs:
# setup disk for littlefs-fuse # setup disk for littlefs-fuse
mkdir mount mkdir mount
sudo chmod a+rw /dev/loop0 LOOP=$(sudo losetup -f)
sudo chmod a+rw $LOOP
dd if=/dev/zero bs=512 count=128K of=disk dd if=/dev/zero bs=512 count=128K of=disk
losetup /dev/loop0 disk losetup $LOOP disk
echo "LOOP=$LOOP" >> $GITHUB_ENV
- name: test - name: test
run: | run: |
# compile v1 and v2 # compile v1 and v2
@@ -358,8 +384,8 @@ jobs:
make -C v2 make -C v2
# run self-host test with v1 # run self-host test with v1
v1/lfs --format /dev/loop0 v1/lfs --format $LOOP
v1/lfs /dev/loop0 mount v1/lfs $LOOP mount
ls mount ls mount
mkdir mount/littlefs mkdir mount/littlefs
@@ -373,8 +399,8 @@ jobs:
cd ../.. cd ../..
fusermount -u mount fusermount -u mount
v2/lfs --migrate /dev/loop0 v2/lfs --migrate $LOOP
v2/lfs /dev/loop0 mount v2/lfs $LOOP mount
# run self-host test with v2 right where we left off # run self-host test with v2 right where we left off
ls mount ls mount
@@ -385,7 +411,7 @@ jobs:
# collect coverage info # collect coverage info
coverage: coverage:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
needs: [test] needs: [test]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -421,14 +447,14 @@ jobs:
export STEP="results-coverage" export STEP="results-coverage"
export CONTEXT="results / coverage" export CONTEXT="results / coverage"
export PREV="$(curl -sS \ export PREV="$(curl -sS \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/status/master" \ "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/status/master?per_page=100" \
| jq -re 'select(.sha != env.GITHUB_SHA) | .statuses[] | jq -re 'select(.sha != env.GITHUB_SHA) | .statuses[]
| select(.context == env.CONTEXT).description | select(.context == env.CONTEXT).description
| capture("Coverage is (?<result>[0-9\\.]+)").result' \ | capture("(?<result>[0-9\\.]+)").result' \
|| echo 0)" || echo 0)"
export DESCRIPTION="$( export DESCRIPTION="$(
./scripts/coverage.py -u results/coverage.csv -s | awk -F '[ /%]+' ' ./scripts/coverage.py -u results/coverage.csv -Y | awk -F '[ /%]+' '
NR==2 {printf "Coverage is %.1f%% of %d lines",$4,$3} NR==2 {printf "%.1f%% of %d lines",$4,$3}
NR==2 && ENVIRON["PREV"]+0 != 0 { NR==2 && ENVIRON["PREV"]+0 != 0 {
printf " (%+.1f%%)",$4-ENVIRON["PREV"]}')" printf " (%+.1f%%)",$4-ENVIRON["PREV"]}')"
jq -n '{ jq -n '{

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@
*.o *.o
*.d *.d
*.a *.a
*.ci
*.csv
# Testing things # Testing things
blocks/ blocks/

View File

@@ -1,3 +1,4 @@
Copyright (c) 2022, The littlefs authors.
Copyright (c) 2017, Arm Limited. All rights reserved. Copyright (c) 2017, Arm Limited. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View File

@@ -17,44 +17,63 @@ TARGET ?= $(BUILDDIR)lfs.a
endif endif
CC ?= gcc CC ?= gcc
AR ?= ar AR ?= ar
SIZE ?= size SIZE ?= size
CTAGS ?= ctags CTAGS ?= ctags
NM ?= nm NM ?= nm
LCOV ?= lcov OBJDUMP ?= objdump
LCOV ?= lcov
SRC ?= $(wildcard *.c) SRC ?= $(wildcard *.c)
OBJ := $(SRC:%.c=$(BUILDDIR)%.o) OBJ := $(SRC:%.c=$(BUILDDIR)%.o)
DEP := $(SRC:%.c=$(BUILDDIR)%.d) DEP := $(SRC:%.c=$(BUILDDIR)%.d)
ASM := $(SRC:%.c=$(BUILDDIR)%.s) ASM := $(SRC:%.c=$(BUILDDIR)%.s)
CGI := $(SRC:%.c=$(BUILDDIR)%.ci)
ifdef DEBUG ifdef DEBUG
override CFLAGS += -O0 -g3 override CFLAGS += -O0
else else
override CFLAGS += -Os override CFLAGS += -Os
endif endif
ifdef TRACE ifdef TRACE
override CFLAGS += -DLFS_YES_TRACE override CFLAGS += -DLFS_YES_TRACE
endif endif
override CFLAGS += -g3
override CFLAGS += -I. override CFLAGS += -I.
override CFLAGS += -std=c99 -Wall -pedantic override CFLAGS += -std=c99 -Wall -pedantic
override CFLAGS += -Wextra -Wshadow -Wjump-misses-init -Wundef override CFLAGS += -Wextra -Wshadow -Wjump-misses-init -Wundef
ifdef VERBOSE ifdef VERBOSE
override TESTFLAGS += -v override TESTFLAGS += -v
override CODEFLAGS += -v override CALLSFLAGS += -v
override CODEFLAGS += -v
override DATAFLAGS += -v
override STACKFLAGS += -v
override STRUCTSFLAGS += -v
override COVERAGEFLAGS += -v override COVERAGEFLAGS += -v
endif endif
ifdef EXEC ifdef EXEC
override TESTFLAGS += --exec="$(EXEC)" override TESTFLAGS += --exec="$(EXEC)"
endif endif
ifdef COVERAGE
override TESTFLAGS += --coverage
endif
ifdef BUILDDIR ifdef BUILDDIR
override TESTFLAGS += --build-dir="$(BUILDDIR:/=)" override TESTFLAGS += --build-dir="$(BUILDDIR:/=)"
override CODEFLAGS += --build-dir="$(BUILDDIR:/=)" override CALLSFLAGS += --build-dir="$(BUILDDIR:/=)"
override CODEFLAGS += --build-dir="$(BUILDDIR:/=)"
override DATAFLAGS += --build-dir="$(BUILDDIR:/=)"
override STACKFLAGS += --build-dir="$(BUILDDIR:/=)"
override STRUCTSFLAGS += --build-dir="$(BUILDDIR:/=)"
override COVERAGEFLAGS += --build-dir="$(BUILDDIR:/=)"
endif endif
ifneq ($(NM),nm) ifneq ($(NM),nm)
override CODEFLAGS += --nm-tool="$(NM)" override CODEFLAGS += --nm-tool="$(NM)"
override DATAFLAGS += --nm-tool="$(NM)"
endif
ifneq ($(OBJDUMP),objdump)
override STRUCTSFLAGS += --objdump-tool="$(OBJDUMP)"
endif endif
@@ -73,9 +92,9 @@ size: $(OBJ)
tags: tags:
$(CTAGS) --totals --c-types=+p $(shell find -H -name '*.h') $(SRC) $(CTAGS) --totals --c-types=+p $(shell find -H -name '*.h') $(SRC)
.PHONY: code .PHONY: calls
code: $(OBJ) calls: $(CGI)
./scripts/code.py $^ $(CODEFLAGS) ./scripts/calls.py $^ $(CALLSFLAGS)
.PHONY: test .PHONY: test
test: test:
@@ -84,9 +103,30 @@ test:
test%: tests/test$$(firstword $$(subst \#, ,%)).toml test%: tests/test$$(firstword $$(subst \#, ,%)).toml
./scripts/test.py $@ $(TESTFLAGS) ./scripts/test.py $@ $(TESTFLAGS)
.PHONY: code
code: $(OBJ)
./scripts/code.py $^ -S $(CODEFLAGS)
.PHONY: data
data: $(OBJ)
./scripts/data.py $^ -S $(DATAFLAGS)
.PHONY: stack
stack: $(CGI)
./scripts/stack.py $^ -S $(STACKFLAGS)
.PHONY: structs
structs: $(OBJ)
./scripts/structs.py $^ -S $(STRUCTSFLAGS)
.PHONY: coverage .PHONY: coverage
coverage: coverage:
./scripts/coverage.py $(BUILDDIR)tests/*.toml.info $(COVERAGEFLAGS) ./scripts/coverage.py $(BUILDDIR)tests/*.toml.info -s $(COVERAGEFLAGS)
.PHONY: summary
summary: $(BUILDDIR)lfs.csv
./scripts/summary.py -Y $^ $(SUMMARYFLAGS)
# rules # rules
-include $(DEP) -include $(DEP)
@@ -95,20 +135,39 @@ coverage:
$(BUILDDIR)lfs: $(OBJ) $(BUILDDIR)lfs: $(OBJ)
$(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@
$(BUILDDIR)%.a: $(OBJ) $(BUILDDIR)lfs.a: $(OBJ)
$(AR) rcs $@ $^ $(AR) rcs $@ $^
$(BUILDDIR)lfs.csv: $(OBJ) $(CGI)
./scripts/code.py $(OBJ) -q $(CODEFLAGS) -o $@
./scripts/data.py $(OBJ) -q -m $@ $(DATAFLAGS) -o $@
./scripts/stack.py $(CGI) -q -m $@ $(STACKFLAGS) -o $@
./scripts/structs.py $(OBJ) -q -m $@ $(STRUCTSFLAGS) -o $@
$(if $(COVERAGE),\
./scripts/coverage.py $(BUILDDIR)tests/*.toml.info \
-q -m $@ $(COVERAGEFLAGS) -o $@)
$(BUILDDIR)%.o: %.c $(BUILDDIR)%.o: %.c
$(CC) -c -MMD $(CFLAGS) $< -o $@ $(CC) -c -MMD $(CFLAGS) $< -o $@
$(BUILDDIR)%.s: %.c $(BUILDDIR)%.s: %.c
$(CC) -S $(CFLAGS) $< -o $@ $(CC) -S $(CFLAGS) $< -o $@
# gcc depends on the output file for intermediate file names, so
# we can't omit to .o output. We also need to serialize with the
# normal .o rule because otherwise we can end up with multiprocess
# problems with two instances of gcc modifying the same .o
$(BUILDDIR)%.ci: %.c | $(BUILDDIR)%.o
$(CC) -c -MMD -fcallgraph-info=su $(CFLAGS) $< -o $|
# clean everything # clean everything
.PHONY: clean .PHONY: clean
clean: clean:
rm -f $(TARGET) rm -f $(BUILDDIR)lfs
rm -f $(BUILDDIR)lfs.a
rm -f $(BUILDDIR)lfs.csv
rm -f $(OBJ) rm -f $(OBJ)
rm -f $(CGI)
rm -f $(DEP) rm -f $(DEP)
rm -f $(ASM) rm -f $(ASM)
rm -f $(BUILDDIR)tests/*.toml.* rm -f $(BUILDDIR)tests/*.toml.*

View File

@@ -1,6 +1,7 @@
/* /*
* Block device emulated in a file * Block device emulated in a file
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
@@ -10,6 +11,10 @@
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#ifdef _WIN32
#include <windows.h>
#endif
int lfs_filebd_createcfg(const struct lfs_config *cfg, const char *path, int lfs_filebd_createcfg(const struct lfs_config *cfg, const char *path,
const struct lfs_filebd_config *bdcfg) { const struct lfs_filebd_config *bdcfg) {
LFS_FILEBD_TRACE("lfs_filebd_createcfg(%p {.context=%p, " LFS_FILEBD_TRACE("lfs_filebd_createcfg(%p {.context=%p, "
@@ -27,7 +32,12 @@ int lfs_filebd_createcfg(const struct lfs_config *cfg, const char *path,
bd->cfg = bdcfg; bd->cfg = bdcfg;
// open file // open file
#ifdef _WIN32
bd->fd = open(path, O_RDWR | O_CREAT | O_BINARY, 0666);
#else
bd->fd = open(path, O_RDWR | O_CREAT, 0666); bd->fd = open(path, O_RDWR | O_CREAT, 0666);
#endif
if (bd->fd < 0) { if (bd->fd < 0) {
int err = -errno; int err = -errno;
LFS_FILEBD_TRACE("lfs_filebd_createcfg -> %d", err); LFS_FILEBD_TRACE("lfs_filebd_createcfg -> %d", err);
@@ -193,7 +203,11 @@ int lfs_filebd_sync(const struct lfs_config *cfg) {
LFS_FILEBD_TRACE("lfs_filebd_sync(%p)", (void*)cfg); LFS_FILEBD_TRACE("lfs_filebd_sync(%p)", (void*)cfg);
// file sync // file sync
lfs_filebd_t *bd = cfg->context; lfs_filebd_t *bd = cfg->context;
#ifdef _WIN32
int err = FlushFileBuffers((HANDLE) _get_osfhandle(fd)) ? 0 : -1;
#else
int err = fsync(bd->fd); int err = fsync(bd->fd);
#endif
if (err) { if (err) {
err = -errno; err = -errno;
LFS_FILEBD_TRACE("lfs_filebd_sync -> %d", 0); LFS_FILEBD_TRACE("lfs_filebd_sync -> %d", 0);

View File

@@ -1,6 +1,7 @@
/* /*
* Block device emulated in a file * Block device emulated in a file
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

View File

@@ -1,6 +1,7 @@
/* /*
* Block device emulated in RAM * Block device emulated in RAM
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

View File

@@ -1,6 +1,7 @@
/* /*
* Block device emulated in RAM * Block device emulated in RAM
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

View File

@@ -2,6 +2,7 @@
* Testing block device, wraps filebd and rambd while providing a bunch * Testing block device, wraps filebd and rambd while providing a bunch
* of hooks for testing littlefs in various conditions. * of hooks for testing littlefs in various conditions.
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

View File

@@ -2,6 +2,7 @@
* Testing block device, wraps filebd and rambd while providing a bunch * Testing block device, wraps filebd and rambd while providing a bunch
* of hooks for testing littlefs in various conditions. * of hooks for testing littlefs in various conditions.
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

1265
lfs.c

File diff suppressed because it is too large Load Diff

8
lfs.h
View File

@@ -1,6 +1,7 @@
/* /*
* The little filesystem * The little filesystem
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */
@@ -22,7 +23,7 @@ extern "C"
// Software library version // Software library version
// Major (top-nibble), incremented on backwards incompatible changes // Major (top-nibble), incremented on backwards incompatible changes
// Minor (bottom-nibble), incremented on feature additions // Minor (bottom-nibble), incremented on feature additions
#define LFS_VERSION 0x00020004 #define LFS_VERSION 0x00020005
#define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16)) #define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16))
#define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0)) #define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0))
@@ -513,6 +514,7 @@ int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type);
/// File operations /// /// File operations ///
#ifndef LFS_NO_MALLOC
// Open a file // Open a file
// //
// The mode that the file is opened in is determined by the flags, which // The mode that the file is opened in is determined by the flags, which
@@ -522,6 +524,10 @@ int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type);
int lfs_file_open(lfs_t *lfs, lfs_file_t *file, int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
const char *path, int flags); const char *path, int flags);
// if LFS_NO_MALLOC is defined, lfs_file_open() will fail with LFS_ERR_NOMEM
// thus use lfs_file_opencfg() with config.buffer set.
#endif
// Open a file with extra configuration // Open a file with extra configuration
// //
// The mode that the file is opened in is determined by the flags, which // The mode that the file is opened in is determined by the flags, which

View File

@@ -1,6 +1,7 @@
/* /*
* lfs util functions * lfs util functions
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

View File

@@ -1,6 +1,7 @@
/* /*
* lfs utility functions * lfs utility functions
* *
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved. * Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause * SPDX-License-Identifier: BSD-3-Clause
*/ */

View File

@@ -15,7 +15,7 @@ import csv
import collections as co import collections as co
OBJ_PATHS = ['*.o', 'bd/*.o'] OBJ_PATHS = ['*.o']
def collect(paths, **args): def collect(paths, **args):
results = co.defaultdict(lambda: 0) results = co.defaultdict(lambda: 0)
@@ -31,7 +31,8 @@ def collect(paths, **args):
proc = sp.Popen(cmd, proc = sp.Popen(cmd,
stdout=sp.PIPE, stdout=sp.PIPE,
stderr=sp.PIPE if not args.get('verbose') else None, stderr=sp.PIPE if not args.get('verbose') else None,
universal_newlines=True) universal_newlines=True,
errors='replace')
for line in proc.stdout: for line in proc.stdout:
m = pattern.match(line) m = pattern.match(line)
if m: if m:
@@ -48,16 +49,30 @@ def collect(paths, **args):
# map to source files # map to source files
if args.get('build_dir'): if args.get('build_dir'):
file = re.sub('%s/*' % re.escape(args['build_dir']), '', file) file = re.sub('%s/*' % re.escape(args['build_dir']), '', file)
# replace .o with .c, different scripts report .o/.c, we need to
# choose one if we want to deduplicate csv files
file = re.sub('\.o$', '.c', file)
# discard internal functions # discard internal functions
if func.startswith('__'): if not args.get('everything'):
continue if func.startswith('__'):
continue
# discard .8449 suffixes created by optimizer # discard .8449 suffixes created by optimizer
func = re.sub('\.[0-9]+', '', func) func = re.sub('\.[0-9]+', '', func)
flat_results.append((file, func, size)) flat_results.append((file, func, size))
return flat_results return flat_results
def main(**args): def main(**args):
def openio(path, mode='r'):
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
else:
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
else:
return open(path, mode)
# find sizes # find sizes
if not args.get('use', None): if not args.get('use', None):
# find .o files # find .o files
@@ -75,13 +90,14 @@ def main(**args):
results = collect(paths, **args) results = collect(paths, **args)
else: else:
with open(args['use']) as f: with openio(args['use']) as f:
r = csv.DictReader(f) r = csv.DictReader(f)
results = [ results = [
( result['file'], ( result['file'],
result['function'], result['name'],
int(result['size'])) int(result['code_size']))
for result in r] for result in r
if result.get('code_size') not in {None, ''}]
total = 0 total = 0
for _, _, size in results: for _, _, size in results:
@@ -89,13 +105,17 @@ def main(**args):
# find previous results? # find previous results?
if args.get('diff'): if args.get('diff'):
with open(args['diff']) as f: try:
r = csv.DictReader(f) with openio(args['diff']) as f:
prev_results = [ r = csv.DictReader(f)
( result['file'], prev_results = [
result['function'], ( result['file'],
int(result['size'])) result['name'],
for result in r] int(result['code_size']))
for result in r
if result.get('code_size') not in {None, ''}]
except FileNotFoundError:
prev_results = []
prev_total = 0 prev_total = 0
for _, _, size in prev_results: for _, _, size in prev_results:
@@ -103,14 +123,34 @@ def main(**args):
# write results to CSV # write results to CSV
if args.get('output'): if args.get('output'):
with open(args['output'], 'w') as f: merged_results = co.defaultdict(lambda: {})
w = csv.writer(f) other_fields = []
w.writerow(['file', 'function', 'size'])
for file, func, size in sorted(results): # merge?
w.writerow((file, func, size)) if args.get('merge'):
try:
with openio(args['merge']) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
func = result.pop('name', '')
result.pop('code_size', None)
merged_results[(file, func)] = result
other_fields = result.keys()
except FileNotFoundError:
pass
for file, func, size in results:
merged_results[(file, func)]['code_size'] = size
with openio(args['output'], 'w') as f:
w = csv.DictWriter(f, ['file', 'name', *other_fields, 'code_size'])
w.writeheader()
for (file, func), result in sorted(merged_results.items()):
w.writerow({'file': file, 'name': func, **result})
# print results # print results
def dedup_entries(results, by='function'): def dedup_entries(results, by='name'):
entries = co.defaultdict(lambda: 0) entries = co.defaultdict(lambda: 0)
for file, func, size in results: for file, func, size in results:
entry = (file if by == 'file' else func) entry = (file if by == 'file' else func)
@@ -126,45 +166,67 @@ def main(**args):
diff[name] = (old, new, new-old, (new-old)/old if old else 1.0) diff[name] = (old, new, new-old, (new-old)/old if old else 1.0)
return diff return diff
def sorted_entries(entries):
if args.get('size_sort'):
return sorted(entries, key=lambda x: (-x[1], x))
elif args.get('reverse_size_sort'):
return sorted(entries, key=lambda x: (+x[1], x))
else:
return sorted(entries)
def sorted_diff_entries(entries):
if args.get('size_sort'):
return sorted(entries, key=lambda x: (-x[1][1], x))
elif args.get('reverse_size_sort'):
return sorted(entries, key=lambda x: (+x[1][1], x))
else:
return sorted(entries, key=lambda x: (-x[1][3], x))
def print_header(by=''): def print_header(by=''):
if not args.get('diff'): if not args.get('diff'):
print('%-36s %7s' % (by, 'size')) print('%-36s %7s' % (by, 'size'))
else: else:
print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff')) print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff'))
def print_entries(by='function'): def print_entry(name, size):
print("%-36s %7d" % (name, size))
def print_diff_entry(name, old, new, diff, ratio):
print("%-36s %7s %7s %+7d%s" % (name,
old or "-",
new or "-",
diff,
' (%+.1f%%)' % (100*ratio) if ratio else ''))
def print_entries(by='name'):
entries = dedup_entries(results, by=by) entries = dedup_entries(results, by=by)
if not args.get('diff'): if not args.get('diff'):
print_header(by=by) print_header(by=by)
for name, size in sorted(entries.items()): for name, size in sorted_entries(entries.items()):
print("%-36s %7d" % (name, size)) print_entry(name, size)
else: else:
prev_entries = dedup_entries(prev_results, by=by) prev_entries = dedup_entries(prev_results, by=by)
diff = diff_entries(prev_entries, entries) diff = diff_entries(prev_entries, entries)
print_header(by='%s (%d added, %d removed)' % (by, print_header(by='%s (%d added, %d removed)' % (by,
sum(1 for old, _, _, _ in diff.values() if not old), sum(1 for old, _, _, _ in diff.values() if not old),
sum(1 for _, new, _, _ in diff.values() if not new))) sum(1 for _, new, _, _ in diff.values() if not new)))
for name, (old, new, diff, ratio) in sorted(diff.items(), for name, (old, new, diff, ratio) in sorted_diff_entries(
key=lambda x: (-x[1][3], x)): diff.items()):
if ratio or args.get('all'): if ratio or args.get('all'):
print("%-36s %7s %7s %+7d%s" % (name, print_diff_entry(name, old, new, diff, ratio)
old or "-",
new or "-",
diff,
' (%+.1f%%)' % (100*ratio) if ratio else ''))
def print_totals(): def print_totals():
if not args.get('diff'): if not args.get('diff'):
print("%-36s %7d" % ('TOTAL', total)) print_entry('TOTAL', total)
else: else:
ratio = (total-prev_total)/prev_total if prev_total else 1.0 ratio = (0.0 if not prev_total and not total
print("%-36s %7s %7s %+7d%s" % ( else 1.0 if not prev_total
'TOTAL', else (total-prev_total)/prev_total)
prev_total if prev_total else '-', print_diff_entry('TOTAL',
total if total else '-', prev_total, total,
total-prev_total, total-prev_total,
' (%+.1f%%)' % (100*ratio) if ratio else '')) ratio)
if args.get('quiet'): if args.get('quiet'):
pass pass
@@ -175,7 +237,7 @@ def main(**args):
print_entries(by='file') print_entries(by='file')
print_totals() print_totals()
else: else:
print_entries(by='function') print_entries(by='name')
print_totals() print_totals()
if __name__ == "__main__": if __name__ == "__main__":
@@ -188,22 +250,30 @@ if __name__ == "__main__":
or a list of paths. Defaults to %r." % OBJ_PATHS) or a list of paths. Defaults to %r." % OBJ_PATHS)
parser.add_argument('-v', '--verbose', action='store_true', parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.") help="Output commands that run behind the scenes.")
parser.add_argument('-q', '--quiet', action='store_true',
help="Don't show anything, useful with -o.")
parser.add_argument('-o', '--output', parser.add_argument('-o', '--output',
help="Specify CSV file to store results.") help="Specify CSV file to store results.")
parser.add_argument('-u', '--use', parser.add_argument('-u', '--use',
help="Don't compile and find code sizes, instead use this CSV file.") help="Don't compile and find code sizes, instead use this CSV file.")
parser.add_argument('-d', '--diff', parser.add_argument('-d', '--diff',
help="Specify CSV file to diff code size against.") help="Specify CSV file to diff code size against.")
parser.add_argument('-m', '--merge',
help="Merge with an existing CSV file when writing to output.")
parser.add_argument('-a', '--all', action='store_true', parser.add_argument('-a', '--all', action='store_true',
help="Show all functions, not just the ones that changed.") help="Show all functions, not just the ones that changed.")
parser.add_argument('--files', action='store_true', parser.add_argument('-A', '--everything', action='store_true',
help="Include builtin and libc specific symbols.")
parser.add_argument('-s', '--size-sort', action='store_true',
help="Sort by size.")
parser.add_argument('-S', '--reverse-size-sort', action='store_true',
help="Sort by size, but backwards.")
parser.add_argument('-F', '--files', action='store_true',
help="Show file-level code sizes. Note this does not include padding! " help="Show file-level code sizes. Note this does not include padding! "
"So sizes may differ from other tools.") "So sizes may differ from other tools.")
parser.add_argument('-s', '--summary', action='store_true', parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the total code size.") help="Only show the total code size.")
parser.add_argument('-q', '--quiet', action='store_true', parser.add_argument('--type', default='tTrRdD',
help="Don't show anything, useful with -o.")
parser.add_argument('--type', default='tTrRdDbB',
help="Type of symbols to report, this uses the same single-character " help="Type of symbols to report, this uses the same single-character "
"type-names emitted by nm. Defaults to %(default)r.") "type-names emitted by nm. Defaults to %(default)r.")
parser.add_argument('--nm-tool', default=['nm'], type=lambda x: x.split(), parser.add_argument('--nm-tool', default=['nm'], type=lambda x: x.split(),

View File

@@ -55,8 +55,9 @@ def collect(paths, **args):
for (file, func), (hits, count) in reduced_funcs.items(): for (file, func), (hits, count) in reduced_funcs.items():
# discard internal/testing functions (test_* injected with # discard internal/testing functions (test_* injected with
# internal testing) # internal testing)
if func.startswith('__') or func.startswith('test_'): if not args.get('everything'):
continue if func.startswith('__') or func.startswith('test_'):
continue
# discard .8449 suffixes created by optimizer # discard .8449 suffixes created by optimizer
func = re.sub('\.[0-9]+', '', func) func = re.sub('\.[0-9]+', '', func)
results.append((file, func, hits, count)) results.append((file, func, hits, count))
@@ -65,6 +66,15 @@ def collect(paths, **args):
def main(**args): def main(**args):
def openio(path, mode='r'):
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
else:
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
else:
return open(path, mode)
# find coverage # find coverage
if not args.get('use'): if not args.get('use'):
# find *.info files # find *.info files
@@ -82,14 +92,16 @@ def main(**args):
results = collect(paths, **args) results = collect(paths, **args)
else: else:
with open(args['use']) as f: with openio(args['use']) as f:
r = csv.DictReader(f) r = csv.DictReader(f)
results = [ results = [
( result['file'], ( result['file'],
result['function'], result['name'],
int(result['hits']), int(result['coverage_hits']),
int(result['count'])) int(result['coverage_count']))
for result in r] for result in r
if result.get('coverage_hits') not in {None, ''}
if result.get('coverage_count') not in {None, ''}]
total_hits, total_count = 0, 0 total_hits, total_count = 0, 0
for _, _, hits, count in results: for _, _, hits, count in results:
@@ -98,14 +110,19 @@ def main(**args):
# find previous results? # find previous results?
if args.get('diff'): if args.get('diff'):
with open(args['diff']) as f: try:
r = csv.DictReader(f) with openio(args['diff']) as f:
prev_results = [ r = csv.DictReader(f)
( result['file'], prev_results = [
result['function'], ( result['file'],
int(result['hits']), result['name'],
int(result['count'])) int(result['coverage_hits']),
for result in r] int(result['coverage_count']))
for result in r
if result.get('coverage_hits') not in {None, ''}
if result.get('coverage_count') not in {None, ''}]
except FileNotFoundError:
prev_results = []
prev_total_hits, prev_total_count = 0, 0 prev_total_hits, prev_total_count = 0, 0
for _, _, hits, count in prev_results: for _, _, hits, count in prev_results:
@@ -114,14 +131,36 @@ def main(**args):
# write results to CSV # write results to CSV
if args.get('output'): if args.get('output'):
with open(args['output'], 'w') as f: merged_results = co.defaultdict(lambda: {})
w = csv.writer(f) other_fields = []
w.writerow(['file', 'function', 'hits', 'count'])
for file, func, hits, count in sorted(results): # merge?
w.writerow((file, func, hits, count)) if args.get('merge'):
try:
with openio(args['merge']) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
func = result.pop('name', '')
result.pop('coverage_hits', None)
result.pop('coverage_count', None)
merged_results[(file, func)] = result
other_fields = result.keys()
except FileNotFoundError:
pass
for file, func, hits, count in results:
merged_results[(file, func)]['coverage_hits'] = hits
merged_results[(file, func)]['coverage_count'] = count
with openio(args['output'], 'w') as f:
w = csv.DictWriter(f, ['file', 'name', *other_fields, 'coverage_hits', 'coverage_count'])
w.writeheader()
for (file, func), result in sorted(merged_results.items()):
w.writerow({'file': file, 'name': func, **result})
# print results # print results
def dedup_entries(results, by='function'): def dedup_entries(results, by='name'):
entries = co.defaultdict(lambda: (0, 0)) entries = co.defaultdict(lambda: (0, 0))
for file, func, hits, count in results: for file, func, hits, count in results:
entry = (file if by == 'file' else func) entry = (file if by == 'file' else func)
@@ -147,23 +186,59 @@ def main(**args):
- (old_hits/old_count if old_count else 1.0))) - (old_hits/old_count if old_count else 1.0)))
return diff return diff
def sorted_entries(entries):
if args.get('coverage_sort'):
return sorted(entries, key=lambda x: (-(x[1][0]/x[1][1] if x[1][1] else -1), x))
elif args.get('reverse_coverage_sort'):
return sorted(entries, key=lambda x: (+(x[1][0]/x[1][1] if x[1][1] else -1), x))
else:
return sorted(entries)
def sorted_diff_entries(entries):
if args.get('coverage_sort'):
return sorted(entries, key=lambda x: (-(x[1][2]/x[1][3] if x[1][3] else -1), x))
elif args.get('reverse_coverage_sort'):
return sorted(entries, key=lambda x: (+(x[1][2]/x[1][3] if x[1][3] else -1), x))
else:
return sorted(entries, key=lambda x: (-x[1][6], x))
def print_header(by=''): def print_header(by=''):
if not args.get('diff'): if not args.get('diff'):
print('%-36s %19s' % (by, 'hits/line')) print('%-36s %19s' % (by, 'hits/line'))
else: else:
print('%-36s %19s %19s %11s' % (by, 'old', 'new', 'diff')) print('%-36s %19s %19s %11s' % (by, 'old', 'new', 'diff'))
def print_entries(by='function'): def print_entry(name, hits, count):
print("%-36s %11s %7s" % (name,
'%d/%d' % (hits, count)
if count else '-',
'%.1f%%' % (100*hits/count)
if count else '-'))
def print_diff_entry(name,
old_hits, old_count,
new_hits, new_count,
diff_hits, diff_count,
ratio):
print("%-36s %11s %7s %11s %7s %11s%s" % (name,
'%d/%d' % (old_hits, old_count)
if old_count else '-',
'%.1f%%' % (100*old_hits/old_count)
if old_count else '-',
'%d/%d' % (new_hits, new_count)
if new_count else '-',
'%.1f%%' % (100*new_hits/new_count)
if new_count else '-',
'%+d/%+d' % (diff_hits, diff_count),
' (%+.1f%%)' % (100*ratio) if ratio else ''))
def print_entries(by='name'):
entries = dedup_entries(results, by=by) entries = dedup_entries(results, by=by)
if not args.get('diff'): if not args.get('diff'):
print_header(by=by) print_header(by=by)
for name, (hits, count) in sorted(entries.items()): for name, (hits, count) in sorted_entries(entries.items()):
print("%-36s %11s %7s" % (name, print_entry(name, hits, count)
'%d/%d' % (hits, count)
if count else '-',
'%.1f%%' % (100*hits/count)
if count else '-'))
else: else:
prev_entries = dedup_entries(prev_results, by=by) prev_entries = dedup_entries(prev_results, by=by)
diff = diff_entries(prev_entries, entries) diff = diff_entries(prev_entries, entries)
@@ -173,45 +248,28 @@ def main(**args):
for name, ( for name, (
old_hits, old_count, old_hits, old_count,
new_hits, new_count, new_hits, new_count,
diff_hits, diff_count, ratio) in sorted(diff.items(), diff_hits, diff_count, ratio) in sorted_diff_entries(
key=lambda x: (-x[1][6], x)): diff.items()):
if ratio or args.get('all'): if ratio or args.get('all'):
print("%-36s %11s %7s %11s %7s %11s%s" % (name, print_diff_entry(name,
'%d/%d' % (old_hits, old_count) old_hits, old_count,
if old_count else '-', new_hits, new_count,
'%.1f%%' % (100*old_hits/old_count) diff_hits, diff_count,
if old_count else '-', ratio)
'%d/%d' % (new_hits, new_count)
if new_count else '-',
'%.1f%%' % (100*new_hits/new_count)
if new_count else '-',
'%+d/%+d' % (diff_hits, diff_count),
' (%+.1f%%)' % (100*ratio) if ratio else ''))
def print_totals(): def print_totals():
if not args.get('diff'): if not args.get('diff'):
print("%-36s %11s %7s" % ('TOTAL', print_entry('TOTAL', total_hits, total_count)
'%d/%d' % (total_hits, total_count)
if total_count else '-',
'%.1f%%' % (100*total_hits/total_count)
if total_count else '-'))
else: else:
ratio = ((total_hits/total_count ratio = ((total_hits/total_count
if total_count else 1.0) if total_count else 1.0)
- (prev_total_hits/prev_total_count - (prev_total_hits/prev_total_count
if prev_total_count else 1.0)) if prev_total_count else 1.0))
print("%-36s %11s %7s %11s %7s %11s%s" % ('TOTAL', print_diff_entry('TOTAL',
'%d/%d' % (prev_total_hits, prev_total_count) prev_total_hits, prev_total_count,
if prev_total_count else '-', total_hits, total_count,
'%.1f%%' % (100*prev_total_hits/prev_total_count) total_hits-prev_total_hits, total_count-prev_total_count,
if prev_total_count else '-', ratio)
'%d/%d' % (total_hits, total_count)
if total_count else '-',
'%.1f%%' % (100*total_hits/total_count)
if total_count else '-',
'%+d/%+d' % (total_hits-prev_total_hits,
total_count-prev_total_count),
' (%+.1f%%)' % (100*ratio) if ratio else ''))
if args.get('quiet'): if args.get('quiet'):
pass pass
@@ -222,7 +280,7 @@ def main(**args):
print_entries(by='file') print_entries(by='file')
print_totals() print_totals()
else: else:
print_entries(by='function') print_entries(by='name')
print_totals() print_totals()
if __name__ == "__main__": if __name__ == "__main__":
@@ -243,12 +301,23 @@ if __name__ == "__main__":
help="Don't do any work, instead use this CSV file.") help="Don't do any work, instead use this CSV file.")
parser.add_argument('-d', '--diff', parser.add_argument('-d', '--diff',
help="Specify CSV file to diff code size against.") help="Specify CSV file to diff code size against.")
parser.add_argument('-m', '--merge',
help="Merge with an existing CSV file when writing to output.")
parser.add_argument('-a', '--all', action='store_true', parser.add_argument('-a', '--all', action='store_true',
help="Show all functions, not just the ones that changed.") help="Show all functions, not just the ones that changed.")
parser.add_argument('--files', action='store_true', parser.add_argument('-A', '--everything', action='store_true',
help="Include builtin and libc specific symbols.")
parser.add_argument('-s', '--coverage-sort', action='store_true',
help="Sort by coverage.")
parser.add_argument('-S', '--reverse-coverage-sort', action='store_true',
help="Sort by coverage, but backwards.")
parser.add_argument('-F', '--files', action='store_true',
help="Show file-level coverage.") help="Show file-level coverage.")
parser.add_argument('-s', '--summary', action='store_true', parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the total coverage.") help="Only show the total coverage.")
parser.add_argument('-q', '--quiet', action='store_true', parser.add_argument('-q', '--quiet', action='store_true',
help="Don't show anything, useful with -o.") help="Don't show anything, useful with -o.")
parser.add_argument('--build-dir',
help="Specify the relative build directory. Used to map object files \
to the correct source files.")
sys.exit(main(**vars(parser.parse_args()))) sys.exit(main(**vars(parser.parse_args())))

283
scripts/data.py Executable file
View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python3
#
# Script to find data size at the function level. Basically just a bit wrapper
# around nm with some extra conveniences for comparing builds. Heavily inspired
# by Linux's Bloat-O-Meter.
#
import os
import glob
import itertools as it
import subprocess as sp
import shlex
import re
import csv
import collections as co
OBJ_PATHS = ['*.o']
def collect(paths, **args):
results = co.defaultdict(lambda: 0)
pattern = re.compile(
'^(?P<size>[0-9a-fA-F]+)' +
' (?P<type>[%s])' % re.escape(args['type']) +
' (?P<func>.+?)$')
for path in paths:
# note nm-tool may contain extra args
cmd = args['nm_tool'] + ['--size-sort', path]
if args.get('verbose'):
print(' '.join(shlex.quote(c) for c in cmd))
proc = sp.Popen(cmd,
stdout=sp.PIPE,
stderr=sp.PIPE if not args.get('verbose') else None,
universal_newlines=True,
errors='replace')
for line in proc.stdout:
m = pattern.match(line)
if m:
results[(path, m.group('func'))] += int(m.group('size'), 16)
proc.wait()
if proc.returncode != 0:
if not args.get('verbose'):
for line in proc.stderr:
sys.stdout.write(line)
sys.exit(-1)
flat_results = []
for (file, func), size in results.items():
# map to source files
if args.get('build_dir'):
file = re.sub('%s/*' % re.escape(args['build_dir']), '', file)
# replace .o with .c, different scripts report .o/.c, we need to
# choose one if we want to deduplicate csv files
file = re.sub('\.o$', '.c', file)
# discard internal functions
if not args.get('everything'):
if func.startswith('__'):
continue
# discard .8449 suffixes created by optimizer
func = re.sub('\.[0-9]+', '', func)
flat_results.append((file, func, size))
return flat_results
def main(**args):
def openio(path, mode='r'):
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
else:
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
else:
return open(path, mode)
# find sizes
if not args.get('use', None):
# find .o files
paths = []
for path in args['obj_paths']:
if os.path.isdir(path):
path = path + '/*.o'
for path in glob.glob(path):
paths.append(path)
if not paths:
print('no .obj files found in %r?' % args['obj_paths'])
sys.exit(-1)
results = collect(paths, **args)
else:
with openio(args['use']) as f:
r = csv.DictReader(f)
results = [
( result['file'],
result['name'],
int(result['data_size']))
for result in r
if result.get('data_size') not in {None, ''}]
total = 0
for _, _, size in results:
total += size
# find previous results?
if args.get('diff'):
try:
with openio(args['diff']) as f:
r = csv.DictReader(f)
prev_results = [
( result['file'],
result['name'],
int(result['data_size']))
for result in r
if result.get('data_size') not in {None, ''}]
except FileNotFoundError:
prev_results = []
prev_total = 0
for _, _, size in prev_results:
prev_total += size
# write results to CSV
if args.get('output'):
merged_results = co.defaultdict(lambda: {})
other_fields = []
# merge?
if args.get('merge'):
try:
with openio(args['merge']) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
func = result.pop('name', '')
result.pop('data_size', None)
merged_results[(file, func)] = result
other_fields = result.keys()
except FileNotFoundError:
pass
for file, func, size in results:
merged_results[(file, func)]['data_size'] = size
with openio(args['output'], 'w') as f:
w = csv.DictWriter(f, ['file', 'name', *other_fields, 'data_size'])
w.writeheader()
for (file, func), result in sorted(merged_results.items()):
w.writerow({'file': file, 'name': func, **result})
# print results
def dedup_entries(results, by='name'):
entries = co.defaultdict(lambda: 0)
for file, func, size in results:
entry = (file if by == 'file' else func)
entries[entry] += size
return entries
def diff_entries(olds, news):
diff = co.defaultdict(lambda: (0, 0, 0, 0))
for name, new in news.items():
diff[name] = (0, new, new, 1.0)
for name, old in olds.items():
_, new, _, _ = diff[name]
diff[name] = (old, new, new-old, (new-old)/old if old else 1.0)
return diff
def sorted_entries(entries):
if args.get('size_sort'):
return sorted(entries, key=lambda x: (-x[1], x))
elif args.get('reverse_size_sort'):
return sorted(entries, key=lambda x: (+x[1], x))
else:
return sorted(entries)
def sorted_diff_entries(entries):
if args.get('size_sort'):
return sorted(entries, key=lambda x: (-x[1][1], x))
elif args.get('reverse_size_sort'):
return sorted(entries, key=lambda x: (+x[1][1], x))
else:
return sorted(entries, key=lambda x: (-x[1][3], x))
def print_header(by=''):
if not args.get('diff'):
print('%-36s %7s' % (by, 'size'))
else:
print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff'))
def print_entry(name, size):
print("%-36s %7d" % (name, size))
def print_diff_entry(name, old, new, diff, ratio):
print("%-36s %7s %7s %+7d%s" % (name,
old or "-",
new or "-",
diff,
' (%+.1f%%)' % (100*ratio) if ratio else ''))
def print_entries(by='name'):
entries = dedup_entries(results, by=by)
if not args.get('diff'):
print_header(by=by)
for name, size in sorted_entries(entries.items()):
print_entry(name, size)
else:
prev_entries = dedup_entries(prev_results, by=by)
diff = diff_entries(prev_entries, entries)
print_header(by='%s (%d added, %d removed)' % (by,
sum(1 for old, _, _, _ in diff.values() if not old),
sum(1 for _, new, _, _ in diff.values() if not new)))
for name, (old, new, diff, ratio) in sorted_diff_entries(
diff.items()):
if ratio or args.get('all'):
print_diff_entry(name, old, new, diff, ratio)
def print_totals():
if not args.get('diff'):
print_entry('TOTAL', total)
else:
ratio = (0.0 if not prev_total and not total
else 1.0 if not prev_total
else (total-prev_total)/prev_total)
print_diff_entry('TOTAL',
prev_total, total,
total-prev_total,
ratio)
if args.get('quiet'):
pass
elif args.get('summary'):
print_header()
print_totals()
elif args.get('files'):
print_entries(by='file')
print_totals()
else:
print_entries(by='name')
print_totals()
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Find data size at the function level.")
parser.add_argument('obj_paths', nargs='*', default=OBJ_PATHS,
help="Description of where to find *.o files. May be a directory \
or a list of paths. Defaults to %r." % OBJ_PATHS)
parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.")
parser.add_argument('-q', '--quiet', action='store_true',
help="Don't show anything, useful with -o.")
parser.add_argument('-o', '--output',
help="Specify CSV file to store results.")
parser.add_argument('-u', '--use',
help="Don't compile and find data sizes, instead use this CSV file.")
parser.add_argument('-d', '--diff',
help="Specify CSV file to diff data size against.")
parser.add_argument('-m', '--merge',
help="Merge with an existing CSV file when writing to output.")
parser.add_argument('-a', '--all', action='store_true',
help="Show all functions, not just the ones that changed.")
parser.add_argument('-A', '--everything', action='store_true',
help="Include builtin and libc specific symbols.")
parser.add_argument('-s', '--size-sort', action='store_true',
help="Sort by size.")
parser.add_argument('-S', '--reverse-size-sort', action='store_true',
help="Sort by size, but backwards.")
parser.add_argument('-F', '--files', action='store_true',
help="Show file-level data sizes. Note this does not include padding! "
"So sizes may differ from other tools.")
parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the total data size.")
parser.add_argument('--type', default='dDbB',
help="Type of symbols to report, this uses the same single-character "
"type-names emitted by nm. Defaults to %(default)r.")
parser.add_argument('--nm-tool', default=['nm'], type=lambda x: x.split(),
help="Path to the nm tool to use.")
parser.add_argument('--build-dir',
help="Specify the relative build directory. Used to map object files \
to the correct source files.")
sys.exit(main(**vars(parser.parse_args())))

430
scripts/stack.py Executable file
View File

@@ -0,0 +1,430 @@
#!/usr/bin/env python3
#
# Script to find stack usage at the function level. Will detect recursion and
# report as infinite stack usage.
#
import os
import glob
import itertools as it
import re
import csv
import collections as co
import math as m
CI_PATHS = ['*.ci']
def collect(paths, **args):
# parse the vcg format
k_pattern = re.compile('([a-z]+)\s*:', re.DOTALL)
v_pattern = re.compile('(?:"(.*?)"|([a-z]+))', re.DOTALL)
def parse_vcg(rest):
def parse_vcg(rest):
node = []
while True:
rest = rest.lstrip()
m = k_pattern.match(rest)
if not m:
return (node, rest)
k, rest = m.group(1), rest[m.end(0):]
rest = rest.lstrip()
if rest.startswith('{'):
v, rest = parse_vcg(rest[1:])
assert rest[0] == '}', "unexpected %r" % rest[0:1]
rest = rest[1:]
node.append((k, v))
else:
m = v_pattern.match(rest)
assert m, "unexpected %r" % rest[0:1]
v, rest = m.group(1) or m.group(2), rest[m.end(0):]
node.append((k, v))
node, rest = parse_vcg(rest)
assert rest == '', "unexpected %r" % rest[0:1]
return node
# collect into functions
results = co.defaultdict(lambda: (None, None, 0, set()))
f_pattern = re.compile(
r'([^\\]*)\\n([^:]*)[^\\]*\\n([0-9]+) bytes \((.*)\)')
for path in paths:
with open(path) as f:
vcg = parse_vcg(f.read())
for k, graph in vcg:
if k != 'graph':
continue
for k, info in graph:
if k == 'node':
info = dict(info)
m = f_pattern.match(info['label'])
if m:
function, file, size, type = m.groups()
if not args.get('quiet') and type != 'static':
print('warning: found non-static stack for %s (%s)'
% (function, type))
_, _, _, targets = results[info['title']]
results[info['title']] = (
file, function, int(size), targets)
elif k == 'edge':
info = dict(info)
_, _, _, targets = results[info['sourcename']]
targets.add(info['targetname'])
else:
continue
if not args.get('everything'):
for source, (s_file, s_function, _, _) in list(results.items()):
# discard internal functions
if s_file.startswith('<') or s_file.startswith('/usr/include'):
del results[source]
# find maximum stack size recursively, this requires also detecting cycles
# (in case of recursion)
def find_limit(source, seen=None):
seen = seen or set()
if source not in results:
return 0
_, _, frame, targets = results[source]
limit = 0
for target in targets:
if target in seen:
# found a cycle
return float('inf')
limit_ = find_limit(target, seen | {target})
limit = max(limit, limit_)
return frame + limit
def find_deps(targets):
deps = set()
for target in targets:
if target in results:
t_file, t_function, _, _ = results[target]
deps.add((t_file, t_function))
return deps
# flatten into a list
flat_results = []
for source, (s_file, s_function, frame, targets) in results.items():
limit = find_limit(source)
deps = find_deps(targets)
flat_results.append((s_file, s_function, frame, limit, deps))
return flat_results
def main(**args):
def openio(path, mode='r'):
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
else:
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
else:
return open(path, mode)
# find sizes
if not args.get('use', None):
# find .ci files
paths = []
for path in args['ci_paths']:
if os.path.isdir(path):
path = path + '/*.ci'
for path in glob.glob(path):
paths.append(path)
if not paths:
print('no .ci files found in %r?' % args['ci_paths'])
sys.exit(-1)
results = collect(paths, **args)
else:
with openio(args['use']) as f:
r = csv.DictReader(f)
results = [
( result['file'],
result['name'],
int(result['stack_frame']),
float(result['stack_limit']), # note limit can be inf
set())
for result in r
if result.get('stack_frame') not in {None, ''}
if result.get('stack_limit') not in {None, ''}]
total_frame = 0
total_limit = 0
for _, _, frame, limit, _ in results:
total_frame += frame
total_limit = max(total_limit, limit)
# find previous results?
if args.get('diff'):
try:
with openio(args['diff']) as f:
r = csv.DictReader(f)
prev_results = [
( result['file'],
result['name'],
int(result['stack_frame']),
float(result['stack_limit']),
set())
for result in r
if result.get('stack_frame') not in {None, ''}
if result.get('stack_limit') not in {None, ''}]
except FileNotFoundError:
prev_results = []
prev_total_frame = 0
prev_total_limit = 0
for _, _, frame, limit, _ in prev_results:
prev_total_frame += frame
prev_total_limit = max(prev_total_limit, limit)
# write results to CSV
if args.get('output'):
merged_results = co.defaultdict(lambda: {})
other_fields = []
# merge?
if args.get('merge'):
try:
with openio(args['merge']) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
func = result.pop('name', '')
result.pop('stack_frame', None)
result.pop('stack_limit', None)
merged_results[(file, func)] = result
other_fields = result.keys()
except FileNotFoundError:
pass
for file, func, frame, limit, _ in results:
merged_results[(file, func)]['stack_frame'] = frame
merged_results[(file, func)]['stack_limit'] = limit
with openio(args['output'], 'w') as f:
w = csv.DictWriter(f, ['file', 'name', *other_fields, 'stack_frame', 'stack_limit'])
w.writeheader()
for (file, func), result in sorted(merged_results.items()):
w.writerow({'file': file, 'name': func, **result})
# print results
def dedup_entries(results, by='name'):
entries = co.defaultdict(lambda: (0, 0, set()))
for file, func, frame, limit, deps in results:
entry = (file if by == 'file' else func)
entry_frame, entry_limit, entry_deps = entries[entry]
entries[entry] = (
entry_frame + frame,
max(entry_limit, limit),
entry_deps | {file if by == 'file' else func
for file, func in deps})
return entries
def diff_entries(olds, news):
diff = co.defaultdict(lambda: (None, None, None, None, 0, 0, 0, set()))
for name, (new_frame, new_limit, deps) in news.items():
diff[name] = (
None, None,
new_frame, new_limit,
new_frame, new_limit,
1.0,
deps)
for name, (old_frame, old_limit, _) in olds.items():
_, _, new_frame, new_limit, _, _, _, deps = diff[name]
diff[name] = (
old_frame, old_limit,
new_frame, new_limit,
(new_frame or 0) - (old_frame or 0),
0 if m.isinf(new_limit or 0) and m.isinf(old_limit or 0)
else (new_limit or 0) - (old_limit or 0),
0.0 if m.isinf(new_limit or 0) and m.isinf(old_limit or 0)
else +float('inf') if m.isinf(new_limit or 0)
else -float('inf') if m.isinf(old_limit or 0)
else +0.0 if not old_limit and not new_limit
else +1.0 if not old_limit
else ((new_limit or 0) - (old_limit or 0))/(old_limit or 0),
deps)
return diff
def sorted_entries(entries):
if args.get('limit_sort'):
return sorted(entries, key=lambda x: (-x[1][1], x))
elif args.get('reverse_limit_sort'):
return sorted(entries, key=lambda x: (+x[1][1], x))
elif args.get('frame_sort'):
return sorted(entries, key=lambda x: (-x[1][0], x))
elif args.get('reverse_frame_sort'):
return sorted(entries, key=lambda x: (+x[1][0], x))
else:
return sorted(entries)
def sorted_diff_entries(entries):
if args.get('limit_sort'):
return sorted(entries, key=lambda x: (-(x[1][3] or 0), x))
elif args.get('reverse_limit_sort'):
return sorted(entries, key=lambda x: (+(x[1][3] or 0), x))
elif args.get('frame_sort'):
return sorted(entries, key=lambda x: (-(x[1][2] or 0), x))
elif args.get('reverse_frame_sort'):
return sorted(entries, key=lambda x: (+(x[1][2] or 0), x))
else:
return sorted(entries, key=lambda x: (-x[1][6], x))
def print_header(by=''):
if not args.get('diff'):
print('%-36s %7s %7s' % (by, 'frame', 'limit'))
else:
print('%-36s %15s %15s %15s' % (by, 'old', 'new', 'diff'))
def print_entry(name, frame, limit):
print("%-36s %7d %7s" % (name,
frame, '' if m.isinf(limit) else int(limit)))
def print_diff_entry(name,
old_frame, old_limit,
new_frame, new_limit,
diff_frame, diff_limit,
ratio):
print('%-36s %7s %7s %7s %7s %+7d %7s%s' % (name,
old_frame if old_frame is not None else "-",
('' if m.isinf(old_limit) else int(old_limit))
if old_limit is not None else "-",
new_frame if new_frame is not None else "-",
('' if m.isinf(new_limit) else int(new_limit))
if new_limit is not None else "-",
diff_frame,
('+∞' if diff_limit > 0 and m.isinf(diff_limit)
else '-∞' if diff_limit < 0 and m.isinf(diff_limit)
else '%+d' % diff_limit),
'' if not ratio
else ' (+∞%)' if ratio > 0 and m.isinf(ratio)
else ' (-∞%)' if ratio < 0 and m.isinf(ratio)
else ' (%+.1f%%)' % (100*ratio)))
def print_entries(by='name'):
# build optional tree of dependencies
def print_deps(entries, depth, print,
filter=lambda _: True,
prefixes=('', '', '', '')):
entries = entries if isinstance(entries, list) else list(entries)
filtered_entries = [(name, entry)
for name, entry in entries
if filter(name)]
for i, (name, entry) in enumerate(filtered_entries):
last = (i == len(filtered_entries)-1)
print(prefixes[0+last] + name, entry)
if depth > 0:
deps = entry[-1]
print_deps(entries, depth-1, print,
lambda name: name in deps,
( prefixes[2+last] + "|-> ",
prefixes[2+last] + "'-> ",
prefixes[2+last] + "| ",
prefixes[2+last] + " "))
entries = dedup_entries(results, by=by)
if not args.get('diff'):
print_header(by=by)
print_deps(
sorted_entries(entries.items()),
args.get('depth') or 0,
lambda name, entry: print_entry(name, *entry[:-1]))
else:
prev_entries = dedup_entries(prev_results, by=by)
diff = diff_entries(prev_entries, entries)
print_header(by='%s (%d added, %d removed)' % (by,
sum(1 for _, old, _, _, _, _, _, _ in diff.values() if old is None),
sum(1 for _, _, _, new, _, _, _, _ in diff.values() if new is None)))
print_deps(
filter(
lambda x: x[1][6] or args.get('all'),
sorted_diff_entries(diff.items())),
args.get('depth') or 0,
lambda name, entry: print_diff_entry(name, *entry[:-1]))
def print_totals():
if not args.get('diff'):
print_entry('TOTAL', total_frame, total_limit)
else:
diff_frame = total_frame - prev_total_frame
diff_limit = (
0 if m.isinf(total_limit or 0) and m.isinf(prev_total_limit or 0)
else (total_limit or 0) - (prev_total_limit or 0))
ratio = (
0.0 if m.isinf(total_limit or 0) and m.isinf(prev_total_limit or 0)
else +float('inf') if m.isinf(total_limit or 0)
else -float('inf') if m.isinf(prev_total_limit or 0)
else 0.0 if not prev_total_limit and not total_limit
else 1.0 if not prev_total_limit
else ((total_limit or 0) - (prev_total_limit or 0))/(prev_total_limit or 0))
print_diff_entry('TOTAL',
prev_total_frame, prev_total_limit,
total_frame, total_limit,
diff_frame, diff_limit,
ratio)
if args.get('quiet'):
pass
elif args.get('summary'):
print_header()
print_totals()
elif args.get('files'):
print_entries(by='file')
print_totals()
else:
print_entries(by='name')
print_totals()
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Find stack usage at the function level.")
parser.add_argument('ci_paths', nargs='*', default=CI_PATHS,
help="Description of where to find *.ci files. May be a directory \
or a list of paths. Defaults to %r." % CI_PATHS)
parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.")
parser.add_argument('-q', '--quiet', action='store_true',
help="Don't show anything, useful with -o.")
parser.add_argument('-o', '--output',
help="Specify CSV file to store results.")
parser.add_argument('-u', '--use',
help="Don't parse callgraph files, instead use this CSV file.")
parser.add_argument('-d', '--diff',
help="Specify CSV file to diff against.")
parser.add_argument('-m', '--merge',
help="Merge with an existing CSV file when writing to output.")
parser.add_argument('-a', '--all', action='store_true',
help="Show all functions, not just the ones that changed.")
parser.add_argument('-A', '--everything', action='store_true',
help="Include builtin and libc specific symbols.")
parser.add_argument('-s', '--limit-sort', action='store_true',
help="Sort by stack limit.")
parser.add_argument('-S', '--reverse-limit-sort', action='store_true',
help="Sort by stack limit, but backwards.")
parser.add_argument('--frame-sort', action='store_true',
help="Sort by stack frame size.")
parser.add_argument('--reverse-frame-sort', action='store_true',
help="Sort by stack frame size, but backwards.")
parser.add_argument('-L', '--depth', default=0, type=lambda x: int(x, 0),
nargs='?', const=float('inf'),
help="Depth of dependencies to show.")
parser.add_argument('-F', '--files', action='store_true',
help="Show file-level calls.")
parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the total stack size.")
parser.add_argument('--build-dir',
help="Specify the relative build directory. Used to map object files \
to the correct source files.")
sys.exit(main(**vars(parser.parse_args())))

331
scripts/structs.py Executable file
View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
#
# Script to find struct sizes.
#
import os
import glob
import itertools as it
import subprocess as sp
import shlex
import re
import csv
import collections as co
OBJ_PATHS = ['*.o']
def collect(paths, **args):
decl_pattern = re.compile(
'^\s+(?P<no>[0-9]+)'
'\s+(?P<dir>[0-9]+)'
'\s+.*'
'\s+(?P<file>[^\s]+)$')
struct_pattern = re.compile(
'^(?:.*DW_TAG_(?P<tag>[a-z_]+).*'
'|^.*DW_AT_name.*:\s*(?P<name>[^:\s]+)\s*'
'|^.*DW_AT_decl_file.*:\s*(?P<decl>[0-9]+)\s*'
'|^.*DW_AT_byte_size.*:\s*(?P<size>[0-9]+)\s*)$')
results = co.defaultdict(lambda: 0)
for path in paths:
# find decl, we want to filter by structs in .h files
decls = {}
# note objdump-tool may contain extra args
cmd = args['objdump_tool'] + ['--dwarf=rawline', path]
if args.get('verbose'):
print(' '.join(shlex.quote(c) for c in cmd))
proc = sp.Popen(cmd,
stdout=sp.PIPE,
stderr=sp.PIPE if not args.get('verbose') else None,
universal_newlines=True,
errors='replace')
for line in proc.stdout:
# find file numbers
m = decl_pattern.match(line)
if m:
decls[int(m.group('no'))] = m.group('file')
proc.wait()
if proc.returncode != 0:
if not args.get('verbose'):
for line in proc.stderr:
sys.stdout.write(line)
sys.exit(-1)
# collect structs as we parse dwarf info
found = False
name = None
decl = None
size = None
# note objdump-tool may contain extra args
cmd = args['objdump_tool'] + ['--dwarf=info', path]
if args.get('verbose'):
print(' '.join(shlex.quote(c) for c in cmd))
proc = sp.Popen(cmd,
stdout=sp.PIPE,
stderr=sp.PIPE if not args.get('verbose') else None,
universal_newlines=True,
errors='replace')
for line in proc.stdout:
# state machine here to find structs
m = struct_pattern.match(line)
if m:
if m.group('tag'):
if (name is not None
and decl is not None
and size is not None):
decl = decls.get(decl, '?')
results[(decl, name)] = size
found = (m.group('tag') == 'structure_type')
name = None
decl = None
size = None
elif found and m.group('name'):
name = m.group('name')
elif found and name and m.group('decl'):
decl = int(m.group('decl'))
elif found and name and m.group('size'):
size = int(m.group('size'))
proc.wait()
if proc.returncode != 0:
if not args.get('verbose'):
for line in proc.stderr:
sys.stdout.write(line)
sys.exit(-1)
flat_results = []
for (file, struct), size in results.items():
# map to source files
if args.get('build_dir'):
file = re.sub('%s/*' % re.escape(args['build_dir']), '', file)
# only include structs declared in header files in the current
# directory, ignore internal-only # structs (these are represented
# in other measurements)
if not args.get('everything'):
if not file.endswith('.h'):
continue
# replace .o with .c, different scripts report .o/.c, we need to
# choose one if we want to deduplicate csv files
file = re.sub('\.o$', '.c', file)
flat_results.append((file, struct, size))
return flat_results
def main(**args):
def openio(path, mode='r'):
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
else:
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
else:
return open(path, mode)
# find sizes
if not args.get('use', None):
# find .o files
paths = []
for path in args['obj_paths']:
if os.path.isdir(path):
path = path + '/*.o'
for path in glob.glob(path):
paths.append(path)
if not paths:
print('no .obj files found in %r?' % args['obj_paths'])
sys.exit(-1)
results = collect(paths, **args)
else:
with openio(args['use']) as f:
r = csv.DictReader(f)
results = [
( result['file'],
result['name'],
int(result['struct_size']))
for result in r
if result.get('struct_size') not in {None, ''}]
total = 0
for _, _, size in results:
total += size
# find previous results?
if args.get('diff'):
try:
with openio(args['diff']) as f:
r = csv.DictReader(f)
prev_results = [
( result['file'],
result['name'],
int(result['struct_size']))
for result in r
if result.get('struct_size') not in {None, ''}]
except FileNotFoundError:
prev_results = []
prev_total = 0
for _, _, size in prev_results:
prev_total += size
# write results to CSV
if args.get('output'):
merged_results = co.defaultdict(lambda: {})
other_fields = []
# merge?
if args.get('merge'):
try:
with openio(args['merge']) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
struct = result.pop('name', '')
result.pop('struct_size', None)
merged_results[(file, struct)] = result
other_fields = result.keys()
except FileNotFoundError:
pass
for file, struct, size in results:
merged_results[(file, struct)]['struct_size'] = size
with openio(args['output'], 'w') as f:
w = csv.DictWriter(f, ['file', 'name', *other_fields, 'struct_size'])
w.writeheader()
for (file, struct), result in sorted(merged_results.items()):
w.writerow({'file': file, 'name': struct, **result})
# print results
def dedup_entries(results, by='name'):
entries = co.defaultdict(lambda: 0)
for file, struct, size in results:
entry = (file if by == 'file' else struct)
entries[entry] += size
return entries
def diff_entries(olds, news):
diff = co.defaultdict(lambda: (0, 0, 0, 0))
for name, new in news.items():
diff[name] = (0, new, new, 1.0)
for name, old in olds.items():
_, new, _, _ = diff[name]
diff[name] = (old, new, new-old, (new-old)/old if old else 1.0)
return diff
def sorted_entries(entries):
if args.get('size_sort'):
return sorted(entries, key=lambda x: (-x[1], x))
elif args.get('reverse_size_sort'):
return sorted(entries, key=lambda x: (+x[1], x))
else:
return sorted(entries)
def sorted_diff_entries(entries):
if args.get('size_sort'):
return sorted(entries, key=lambda x: (-x[1][1], x))
elif args.get('reverse_size_sort'):
return sorted(entries, key=lambda x: (+x[1][1], x))
else:
return sorted(entries, key=lambda x: (-x[1][3], x))
def print_header(by=''):
if not args.get('diff'):
print('%-36s %7s' % (by, 'size'))
else:
print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff'))
def print_entry(name, size):
print("%-36s %7d" % (name, size))
def print_diff_entry(name, old, new, diff, ratio):
print("%-36s %7s %7s %+7d%s" % (name,
old or "-",
new or "-",
diff,
' (%+.1f%%)' % (100*ratio) if ratio else ''))
def print_entries(by='name'):
entries = dedup_entries(results, by=by)
if not args.get('diff'):
print_header(by=by)
for name, size in sorted_entries(entries.items()):
print_entry(name, size)
else:
prev_entries = dedup_entries(prev_results, by=by)
diff = diff_entries(prev_entries, entries)
print_header(by='%s (%d added, %d removed)' % (by,
sum(1 for old, _, _, _ in diff.values() if not old),
sum(1 for _, new, _, _ in diff.values() if not new)))
for name, (old, new, diff, ratio) in sorted_diff_entries(
diff.items()):
if ratio or args.get('all'):
print_diff_entry(name, old, new, diff, ratio)
def print_totals():
if not args.get('diff'):
print_entry('TOTAL', total)
else:
ratio = (0.0 if not prev_total and not total
else 1.0 if not prev_total
else (total-prev_total)/prev_total)
print_diff_entry('TOTAL',
prev_total, total,
total-prev_total,
ratio)
if args.get('quiet'):
pass
elif args.get('summary'):
print_header()
print_totals()
elif args.get('files'):
print_entries(by='file')
print_totals()
else:
print_entries(by='name')
print_totals()
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Find struct sizes.")
parser.add_argument('obj_paths', nargs='*', default=OBJ_PATHS,
help="Description of where to find *.o files. May be a directory \
or a list of paths. Defaults to %r." % OBJ_PATHS)
parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.")
parser.add_argument('-q', '--quiet', action='store_true',
help="Don't show anything, useful with -o.")
parser.add_argument('-o', '--output',
help="Specify CSV file to store results.")
parser.add_argument('-u', '--use',
help="Don't compile and find struct sizes, instead use this CSV file.")
parser.add_argument('-d', '--diff',
help="Specify CSV file to diff struct size against.")
parser.add_argument('-m', '--merge',
help="Merge with an existing CSV file when writing to output.")
parser.add_argument('-a', '--all', action='store_true',
help="Show all functions, not just the ones that changed.")
parser.add_argument('-A', '--everything', action='store_true',
help="Include builtin and libc specific symbols.")
parser.add_argument('-s', '--size-sort', action='store_true',
help="Sort by size.")
parser.add_argument('-S', '--reverse-size-sort', action='store_true',
help="Sort by size, but backwards.")
parser.add_argument('-F', '--files', action='store_true',
help="Show file-level struct sizes.")
parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the total struct size.")
parser.add_argument('--objdump-tool', default=['objdump'], type=lambda x: x.split(),
help="Path to the objdump tool to use.")
parser.add_argument('--build-dir',
help="Specify the relative build directory. Used to map object files \
to the correct source files.")
sys.exit(main(**vars(parser.parse_args())))

279
scripts/summary.py Executable file
View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3
#
# Script to summarize the outputs of other scripts. Operates on CSV files.
#
import functools as ft
import collections as co
import os
import csv
import re
import math as m
# displayable fields
Field = co.namedtuple('Field', 'name,parse,acc,key,fmt,repr,null,ratio')
FIELDS = [
# name, parse, accumulate, fmt, print, null
Field('code',
lambda r: int(r['code_size']),
sum,
lambda r: r,
'%7s',
lambda r: r,
'-',
lambda old, new: (new-old)/old),
Field('data',
lambda r: int(r['data_size']),
sum,
lambda r: r,
'%7s',
lambda r: r,
'-',
lambda old, new: (new-old)/old),
Field('stack',
lambda r: float(r['stack_limit']),
max,
lambda r: r,
'%7s',
lambda r: '' if m.isinf(r) else int(r),
'-',
lambda old, new: (new-old)/old),
Field('structs',
lambda r: int(r['struct_size']),
sum,
lambda r: r,
'%8s',
lambda r: r,
'-',
lambda old, new: (new-old)/old),
Field('coverage',
lambda r: (int(r['coverage_hits']), int(r['coverage_count'])),
lambda rs: ft.reduce(lambda a, b: (a[0]+b[0], a[1]+b[1]), rs),
lambda r: r[0]/r[1],
'%19s',
lambda r: '%11s %7s' % ('%d/%d' % (r[0], r[1]), '%.1f%%' % (100*r[0]/r[1])),
'%11s %7s' % ('-', '-'),
lambda old, new: ((new[0]/new[1]) - (old[0]/old[1])))
]
def main(**args):
def openio(path, mode='r'):
if path == '-':
if 'r' in mode:
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
else:
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
else:
return open(path, mode)
# find results
results = co.defaultdict(lambda: {})
for path in args.get('csv_paths', '-'):
try:
with openio(path) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
name = result.pop('name', '')
prev = results[(file, name)]
for field in FIELDS:
try:
r = field.parse(result)
if field.name in prev:
results[(file, name)][field.name] = field.acc(
[prev[field.name], r])
else:
results[(file, name)][field.name] = r
except (KeyError, ValueError):
pass
except FileNotFoundError:
pass
# find fields
if args.get('all_fields'):
fields = FIELDS
elif args.get('fields') is not None:
fields_dict = {field.name: field for field in FIELDS}
fields = [fields_dict[f] for f in args['fields']]
else:
fields = []
for field in FIELDS:
if any(field.name in result for result in results.values()):
fields.append(field)
# find total for every field
total = {}
for result in results.values():
for field in fields:
if field.name in result and field.name in total:
total[field.name] = field.acc(
[total[field.name], result[field.name]])
elif field.name in result:
total[field.name] = result[field.name]
# find previous results?
if args.get('diff'):
prev_results = co.defaultdict(lambda: {})
try:
with openio(args['diff']) as f:
r = csv.DictReader(f)
for result in r:
file = result.pop('file', '')
name = result.pop('name', '')
prev = prev_results[(file, name)]
for field in FIELDS:
try:
r = field.parse(result)
if field.name in prev:
prev_results[(file, name)][field.name] = field.acc(
[prev[field.name], r])
else:
prev_results[(file, name)][field.name] = r
except (KeyError, ValueError):
pass
except FileNotFoundError:
pass
prev_total = {}
for result in prev_results.values():
for field in fields:
if field.name in result and field.name in prev_total:
prev_total[field.name] = field.acc(
[prev_total[field.name], result[field.name]])
elif field.name in result:
prev_total[field.name] = result[field.name]
# print results
def dedup_entries(results, by='name'):
entries = co.defaultdict(lambda: {})
for (file, func), result in results.items():
entry = (file if by == 'file' else func)
prev = entries[entry]
for field in fields:
if field.name in result and field.name in prev:
entries[entry][field.name] = field.acc(
[prev[field.name], result[field.name]])
elif field.name in result:
entries[entry][field.name] = result[field.name]
return entries
def sorted_entries(entries):
if args.get('sort') is not None:
field = {field.name: field for field in FIELDS}[args['sort']]
return sorted(entries, key=lambda x: (
-(field.key(x[1][field.name])) if field.name in x[1] else -1, x))
elif args.get('reverse_sort') is not None:
field = {field.name: field for field in FIELDS}[args['reverse_sort']]
return sorted(entries, key=lambda x: (
+(field.key(x[1][field.name])) if field.name in x[1] else -1, x))
else:
return sorted(entries)
def print_header(by=''):
if not args.get('diff'):
print('%-36s' % by, end='')
for field in fields:
print((' '+field.fmt) % field.name, end='')
print()
else:
print('%-36s' % by, end='')
for field in fields:
print((' '+field.fmt) % field.name, end='')
print(' %-9s' % '', end='')
print()
def print_entry(name, result):
print('%-36s' % name, end='')
for field in fields:
r = result.get(field.name)
if r is not None:
print((' '+field.fmt) % field.repr(r), end='')
else:
print((' '+field.fmt) % '-', end='')
print()
def print_diff_entry(name, old, new):
print('%-36s' % name, end='')
for field in fields:
n = new.get(field.name)
if n is not None:
print((' '+field.fmt) % field.repr(n), end='')
else:
print((' '+field.fmt) % '-', end='')
o = old.get(field.name)
ratio = (
0.0 if m.isinf(o or 0) and m.isinf(n or 0)
else +float('inf') if m.isinf(n or 0)
else -float('inf') if m.isinf(o or 0)
else 0.0 if not o and not n
else +1.0 if not o
else -1.0 if not n
else field.ratio(o, n))
print(' %-9s' % (
'' if not ratio
else '(+∞%)' if ratio > 0 and m.isinf(ratio)
else '(-∞%)' if ratio < 0 and m.isinf(ratio)
else '(%+.1f%%)' % (100*ratio)), end='')
print()
def print_entries(by='name'):
entries = dedup_entries(results, by=by)
if not args.get('diff'):
print_header(by=by)
for name, result in sorted_entries(entries.items()):
print_entry(name, result)
else:
prev_entries = dedup_entries(prev_results, by=by)
print_header(by='%s (%d added, %d removed)' % (by,
sum(1 for name in entries if name not in prev_entries),
sum(1 for name in prev_entries if name not in entries)))
for name, result in sorted_entries(entries.items()):
if args.get('all') or result != prev_entries.get(name, {}):
print_diff_entry(name, prev_entries.get(name, {}), result)
def print_totals():
if not args.get('diff'):
print_entry('TOTAL', total)
else:
print_diff_entry('TOTAL', prev_total, total)
if args.get('summary'):
print_header()
print_totals()
elif args.get('files'):
print_entries(by='file')
print_totals()
else:
print_entries(by='name')
print_totals()
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Summarize measurements")
parser.add_argument('csv_paths', nargs='*', default='-',
help="Description of where to find *.csv files. May be a directory \
or list of paths. *.csv files will be merged to show the total \
coverage.")
parser.add_argument('-d', '--diff',
help="Specify CSV file to diff against.")
parser.add_argument('-a', '--all', action='store_true',
help="Show all objects, not just the ones that changed.")
parser.add_argument('-e', '--all-fields', action='store_true',
help="Show all fields, even those with no results.")
parser.add_argument('-f', '--fields', type=lambda x: re.split('\s*,\s*', x),
help="Comma separated list of fields to print, by default all fields \
that are found in the CSV files are printed.")
parser.add_argument('-s', '--sort',
help="Sort by this field.")
parser.add_argument('-S', '--reverse-sort',
help="Sort by this field, but backwards.")
parser.add_argument('-F', '--files', action='store_true',
help="Show file-level calls.")
parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the totals.")
sys.exit(main(**vars(parser.parse_args())))

View File

@@ -784,10 +784,13 @@ def main(**args):
stdout=sp.PIPE if not args.get('verbose') else None, stdout=sp.PIPE if not args.get('verbose') else None,
stderr=sp.STDOUT if not args.get('verbose') else None, stderr=sp.STDOUT if not args.get('verbose') else None,
universal_newlines=True) universal_newlines=True)
stdout = []
for line in proc.stdout:
stdout.append(line)
proc.wait() proc.wait()
if proc.returncode != 0: if proc.returncode != 0:
if not args.get('verbose'): if not args.get('verbose'):
for line in proc.stdout: for line in stdout:
sys.stdout.write(line) sys.stdout.write(line)
sys.exit(-1) sys.exit(-1)
@@ -803,9 +806,9 @@ def main(**args):
failure.case.test(failure=failure, **args) failure.case.test(failure=failure, **args)
sys.exit(0) sys.exit(0)
print('tests passed %d/%d (%.2f%%)' % (passed, total, print('tests passed %d/%d (%.1f%%)' % (passed, total,
100*(passed/total if total else 1.0))) 100*(passed/total if total else 1.0)))
print('tests failed %d/%d (%.2f%%)' % (failed, total, print('tests failed %d/%d (%.1f%%)' % (failed, total,
100*(failed/total if total else 1.0))) 100*(failed/total if total else 1.0)))
return 1 if failed > 0 else 0 return 1 if failed > 0 else 0