Hlavní navigace

Trasování aplikací naprogramovaných v Pythonu

9. 2. 2021
Doba čtení: 24 minut

Sdílet

Seznámíme se s trojicí nástrojů určených pro trasování (sledování) aplikací v Pythonu. Kromě standardního nástroje „trace“ se jedná o pomocnou utilitu „coverage“ a především o nástroj „pycrunch-pytrace“ s vlastním GUI.

Obsah

1. Krátká lekce z historie: příkazy TRONTROFF

2. Standardní modul trace

3. Zjištění počtu průchodů jednotlivými řádky skriptu či skriptů

4. Detekce řádků s „mrtvým“ kódem

5. Tisk seznamu funkcí, které byly zavolány

6. Seznam se vzájemnými vztahy mezi funkcemi (které funkce jsou volány)

7. Úplné trasování, informace o relativních časech při trasování

8. Pomocný nástroj coverage.py

9. Spuštění aplikace přes nástroj coverage.py

10. Zobrazení výsledků

11. Anotace zdrojového kódu

12. Export výsledků do formátů JSON a XML

13. Zobrazení výsledků ve formě vygenerovaných HTML stránek

14. Interní struktura s informacemi o trasování (SQLite databáze)

15. Nástroj pycrunch-pytrace

16. Instalace nástroje pycrunch-pytrace

17. Úprava trasovaného skriptu a spuštění trasování

18. Prohlédnutí výsledků trasování v GUI

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Krátká lekce z historie: příkazy TRONTROFF

Již starodávný interpret BASICu pojmenovaný GW-BASIC, který pochází z roku 1983, obsahoval základní prostředky použitelné pro ladění aplikací. Zatímco v současných vývojových prostředích je běžné, že uživatel může postupně krokovat jednotlivými příkazy laděné aplikace a přitom si vypisovat obsah proměnných (popř. obsah proměnných měnit či modifikovat vlastní programový kód), na počátku osmdesátých let minulého století nebyly dostupné vývojové prostředky na mikropočítačích zdaleka tak dokonalé. V GW-BASICu bylo samozřejmě možné, podobně jako v prakticky všech interpretrech BASICu, vypisovat hodnoty proměnných či celých výrazů, spouštět program od místa kde byl zastaven, provést skok na libovolný programový řádek atd. Navíc však GW-BASIC obsahoval i velmi užitečný příkaz TRON (trace on), který prováděl přesně to, co jeho název napovídá – po spuštění programu vypisoval čísla řádků, kterými interpret při běhu programu prošel, takže programátor mohl po ukončení (pádu :-) programu alespoň částečně zrekonstruovat, co jeho aplikace ve skutečnosti prováděla.

TROFF

Obrázek 1: Program spuštěný (interpretovaný) běžných způsobem, na obrazovku vypisuje pouze svůj výstup – sekvenci čísel 1 až 10.

Na druhém obrázku je ukázáno, jak se bude běh (tj. ve skutečnosti interpretace) programu lišit po zadání příkazu TRON. Pokud je tento příkaz aktivní, vypisují se při interpretaci programu do hranatých závorek čísla programových řádků, kterými program při svém běhu prochází. V demonstračním příkladu ukázaném na druhém obrázku jsou čísla řádků prokládána samotným výstupem programu, konkrétně výsledkem příkazu PRINT A. Pokud by program na výstup neposílal žádný text, byla by výsledkem jeho běhu pouze řada čísel umístěná do hranatých závorek. Příkaz TRON je možné deaktivovat příkazem TROFF (trace off).

TRON

Obrázek 2: Běh programu po zadání příkazu TRON.

Poznámka: v některých materiálech se můžeme dočíst, že název známého filmu TRON, v němž byla jako v jednom z prvních filmů použita trojrozměrná renderovaná počítačová grafika, je odvozen právě do BASICovského příkazu TRON, ovšem režisér Steven Lisberger to popírá a tvrdí, že název filmu získal ze slova „electronic“.

2. Standardní modul trace

Vývojáři používající jazyk Python mají k dispozici hned několik nástrojů určených pro sledování (trasování) běhu programu. Jeden z těchto nástrojů se jmenuje příznačně trace a jeho nespornou předností je fakt, že je již součástí standardní knihovny Pythonu (tedy patří do skupiny nástrojů shrnutých pod heslem „batteries included“). Příklad použití tohoto nástroje si ukážeme na velmi jednoduchém skriptu, jenž obsahuje implementaci rekurzivního algoritmu pro výpočet faktoriálu. Jedná se tedy o klasický „školní“ algoritmus, který pravděpodobně není nutné podrobněji popisovat:

"""Výpočet faktoriálu."""
 
 
def factorial(n):
    """Rekurzivní výpočet faktoriálu."""
    assert isinstance(n, int), "Integer expected"
 
    if n < 0:
        return None
    if n == 0:
        return 1
    result = n * factorial(n-1)
 
    assert isinstance(result, int), "Internal error in factorial computation"
    return result
 
 
def main():
    for n in range(0, 11):
        print(n, factorial(n))
 
 
if __name__ == "__main__":
    main()

3. Zjištění počtu průchodů jednotlivými řádky skriptu či skriptů

První informací, kterou s využitím základního modulu trace můžeme zjistit, je počet průchodů jednotlivými řádky spouštěného skriptu nebo skriptů (pochopitelně psaných v Pythonu). Pokud například budeme chtít zjistit, kolikrát jsou provedeny (vykonány) řádky ve skriptu pro výpočet faktoriálu, postačuje použít tento příkaz:

$ python3 -m trace --count -C . factorial.py

Výsledkem by měl být soubor nazvaný factorial.cover s tímto obsahem:

    1: """Výpočet faktoriálu."""
 
 
    1: def factorial(n):
           """Rekurzivní výpočet faktoriálu."""
   66:     assert isinstance(n, int), "Integer expected"
 
   66:     if n < 0:
               return None
   66:     if n == 0:
   11:         return 1
   55:     result = n * factorial(n-1)
 
   55:     assert isinstance(result, int), "Internal error in factorial computation"
   55:     return result
 
 
    1: def main():
   12:     for n in range(0, 11):
   11:         print(n, factorial(n))
 
 
    1: if __name__ == "__main__":
    1:     main()

Povšimněte si, že i dokumentační řetězce jsou spuštěny a vyhodnocovány (jde totiž o výrazy, jejichž výsledek je zahozen). To si ostatně můžeme snadno ověřit:

"""Test."""
# test
"Test."
# test

Výsledek trasování tohoto skriptu:

    1: """Test."""
       # test
    1: "Test."
       # test

Vidíme, že dokumentační i běžné řetězce jsou skutečně považovány za příkazy, kdežto komentáře nikoli.

4. Detekce řádků s „mrtvým“ kódem

Ve výpisu z předchozí kapitoly se nachází i jeden řádek, který nebyl spuštěn ani jednou:

    1: """Výpočet faktoriálu."""
 
 
    1: def factorial(n):
           """Rekurzivní výpočet faktoriálu."""
   66:     assert isinstance(n, int), "Integer expected"
 
   66:     if n < 0:
               return None
   66:     if n == 0:
   11:         return 1
   55:     result = n * factorial(n-1)
 
   55:     assert isinstance(result, int), "Internal error in factorial computation"
   55:     return result
 
 
    1: def main():
   12:     for n in range(0, 11):
   11:         print(n, factorial(n))
 
 
    1: if __name__ == "__main__":
    1:     main()

V delším skriptu je mnohdy obtížné takové řádky najít; proto modul trace podporuje volbu -m, která takové „mrtvé“ příkazy zvýrazní:

$ python3 -m trace --count -m -C . factorial.py

Nyní vypadá výsledek následovně – řádek s „mrtvým“ kódem je zvýrazněn pomocí znaků >>>>>>:

    1: """Výpočet faktoriálu."""
 
 
    1: def factorial(n):
           """Rekurzivní výpočet faktoriálu."""
   66:     assert isinstance(n, int), "Integer expected"
 
   66:     if n < 0:
>>>>>>         return None
   66:     if n == 0:
   11:         return 1
   55:     result = n * factorial(n-1)
 
   55:     assert isinstance(result, int), "Internal error in factorial computation"
   55:     return result
 
 
    1: def main():
   12:     for n in range(0, 11):
   11:         print(n, factorial(n))
 
 
    1: if __name__ == "__main__":
    1:     main()

Zobrazit je možné i souhrnné informace o skriptech, jejichž řádky byly při spuštění vyhodnoceny:

$ python3 -m trace --count --summary -C . factorial.py

Po spuštění předchozího příkazu se nejdříve zobrazí výstup produkovaný samotným skriptem a posléze i souhrnné informace:

0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
10 3628800
 
lines   cov%   module   (path)
   14   100%   factorial   (factorial.py)
    1   100%   trace   (/usr/lib64/python3.6/trace.py)

5. Tisk seznamu funkcí, které byly zavolány

Mnohdy nám postačuje znát pouze informace o funkcích (a pochopitelně i o metodách), které byly při běhu skriptu zavolány. Jedná se o odlišný režim nástroje trace, který se vybírá volbou –listfuncs tak, jak je to ukázáno v dalším příkladu:

$ python3 -m trace --listfuncs -C . factorial.py

Nejprve se opět vypíšou výsledky produkované přímo při výpočtu faktoriálu:

0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
10 3628800

Nakonec získáme seznam volaných funkcí, včetně pseudofunkce volané při vstupu do modulu factorial:

functions called:
filename: /usr/lib64/python3.6/trace.py, modulename: trace, funcname: _unsettrace
filename: factorial.py, modulename: factorial, funcname: <module>
filename: factorial.py, modulename: factorial, funcname: factorial
filename: factorial.py, modulename: factorial, funcname: main
Poznámka: zde je počet zobrazených informací nižší, než v předchozích dvou příkladech, ovšem mnohdy plně dostačuje.

6. Seznam se vzájemnými vztahy mezi funkcemi (které funkce jsou volány)

Další potenciálně velmi užitečnou informací jsou vzájemné vztahy mezi funkcemi, přesněji řečeno informace o tom, z jaké funkce došlo k volání jiné či stejné (přímá rekurze) funkce. I tyto informace lze standardním nástrojem trace získat, a to konkrétně při použití přepínače –trackcalls. Opět se podívejme na demonstrační příklad:

$ python3 -m trace --trackcalls -C . factorial.py

Po již obligátním výpisu tabulky s faktoriály…

0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
10 3628800

… se zobrazí následující řádky nabízející informace o dynamických vztazích mezi funkcemi, tedy o vztazích, které mnohdy nelze získat pouhou statickou analýzou zdrojových kódů:

calling relationships:
 
*** /usr/lib64/python3.6/trace.py ***
    trace.Trace.runctx -> trace._unsettrace
  --> factorial.py
    trace.Trace.runctx -> factorial.<module>
 
*** factorial.py ***
    factorial.<module> -> factorial.main
    factorial.factorial -> factorial.factorial
    factorial.main -> factorial.factorial

7. Úplné trasování, informace o relativních časech při trasování

Poslední vlastnost modulu trace, s níž se dnes seznámíme, je možnost přímo při běhu skriptu současně vypisovat programové řádky, které se právě provádí. Jedná se vlastně o vylepšenou variantu příkazu TRON, která v mnoha BASICech vypisovala pouze čísla řádků. V případě Pythonu a modulu trace se vypisují celé řádky, nikoli jejich číslo (které má mimo BASIC jen omezený význam). Podívejme se nyní na základní příklad použití:

$ python3 -m trace --trace -C . factorial.py > factorial.trace

Po spuštění takto zapsaného příkazu se postupně začnou jednotlivé spouštěné řádky vypisovat ve formátu jméno souboru(číslo řádku): programový řádek:

 --- modulename: factorial, funcname: <module>
factorial.py(1): """Výpočet faktoriálu."""
factorial.py(4): def factorial(n):
factorial.py(18): def main():
factorial.py(23): if __name__ == "__main__":
factorial.py(24):     main()
 --- modulename: factorial, funcname: main
factorial.py(19):     for n in range(0, 11):
factorial.py(20):         print(n, factorial(n))
 --- modulename: factorial, funcname: factorial
factorial.py(6):     assert isinstance(n, int), "Integer expected"
factorial.py(8):     if n < 0:
factorial.py(10):     if n == 0:
factorial.py(11):         return 1
    ...
    ...
    ...
Poznámka: celý soubor vygenerovaný předchozím příkazem je dostupný na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial.trace.

Navíc je možné ke každému vypsanému řádku přidat i relativní čas (v sekundách) počítaný od doby spuštění programu:

$ python3 -m trace --trace -g -C . factorial.py > factorial.timing

S výsledkem:

 --- modulename: factorial, funcname:
0.00 factorial.py(1): """Výpočet faktoriálu."""
0.00 factorial.py(4): def factorial(n):
0.00 factorial.py(18): def main():
0.00 factorial.py(23): if __name__ == "__main__":
0.00 factorial.py(24):     main()
 --- modulename: factorial, funcname: main
0.00 factorial.py(19):     for n in range(0, 11):
0.00 factorial.py(20):         print(n, factorial(n))
 --- modulename: factorial, funcname: factorial
0.00 factorial.py(6):     assert isinstance(n, int), "Integer expected"
0.00 factorial.py(8):     if n < 0:
0.00 factorial.py(10):     if n == 0:
0.00 factorial.py(11):         return 1
    ...
    ...
    ...
Poznámka: celý soubor vygenerovaný předchozím příkazem je dostupný na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial.timing.

8. Pomocný nástroj coverage.py

V seriálu o testování v Pythonu jsme se mj. seznámili i s užitečným nástrojem pytest-cov, který dokáže zjistit, které části kódu jsou pokryté jednotkovými testy (tedy které programové řádky jsou skutečně zavolány). Podobnou informaci je ovšem možné zjistit i pro běžně spouštěné skripty – tedy jde nám o výpis informací o těch řádcích kódu, které byly skutečně spuštěny a které naopak nebyly, a to buď z toho důvodu, že nebyly splněny nějaké podmínky, nedošlo k chybě, popř. jsou dané řádky zbytečné z důvodu logické chyby v přepisovaném algoritmu. Tyto informace lze v přehledné podobě získat s využitím nástroje coverage.py, který existuje i ve výše zmíněné variantě pytest-cov.

Instalace nástroje coverage.py je přímočará, protože se jedná o balíček dostupný na PyPi:

$ pip3 install --user coverage

Průběh instalace:

Collecting coverage
  Downloading https://files.pythonhosted.org/packages/5a/0d/a1cb46ee9b9f6369e2bcf72b8277654a806bdf3f17f724be24a3a72afdc3/coverage-5.4-cp38-cp38-manylinux2010_x86_64.whl (245kB)
     |████████████████████████████████| 245kB 1.9MB/s
Installing collected packages: coverage
Successfully installed coverage-5.4

Po instalaci se přesvědčíme, že je nástroj coverage.py volatelný přímo z příkazové řádky:

$ coverage help
 
Coverage.py, version 5.4 with C extension
Measure, collect, and report on code coverage in Python programs.
 
usage: coverage <command> [options] [args]
 
Commands:
    annotate    Annotate source files with execution information.
    combine     Combine a number of data files.
    debug       Display information about the internals of coverage.py
    erase       Erase previously collected coverage data.
    help        Get help on using coverage.py.
    html        Create an HTML report.
    json        Create a JSON report of coverage results.
    report      Report coverage stats on modules.
    run         Run a Python program and measure code execution.
    xml         Create an XML report of coverage results.
 
Use "coverage help <command>" for detailed help on any command.
Full documentation is at https://coverage.readthedocs.io

9. Spuštění aplikace přes nástroj coverage.py

Libovolný skript psaný v Pythonu by nyní mělo být možné spustit nepřímo příkazem coverage, tedy následujícím způsobem:

$ coverage run factorial.py

Skript se zdánlivě spustí naprosto normálním způsobem:

0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
10 3628800

Ve skutečnosti se ovšem průběžně vytváří soubor .coverage, jehož obsah je dále zpracovatelný, což si ostatně ukážeme v navazujících kapitolách.

10. Zobrazení výsledků

Výsledky získané nástrojem coverage je možné zobrazit v několika podobách. Základem je tabulka obsahující jména jednotlivých souborů se zdrojovými kódy, počet příkazů (statements) v každém souboru, počet příkazů, které nebyly vykonány a konečně poměr vykonaných příkazů ke všem příkazům.

Pro náš jednoduchý příklad s jediným skriptem bude výsledek vypadat následovně:

$ coverage report
 
Name           Stmts   Miss  Cover
----------------------------------
factorial.py      14      1    93%
----------------------------------
TOTAL             14      1    93%

Z výsledků můžeme vyčíst informaci, kterou již známe – jeden z příkazů nebyl použit.

Pro složitější aplikaci skládající se z několika souborů se zdrojovými texty bude výsledek vypadat takto:

$ coverage report
 
Name                                     Stmts   Miss  Cover
------------------------------------------------------------
src/gui/__init__.py                          0      0   100%
src/gui/canvas.py                            7      4    43%
src/gui/dialogs/__init__.py                  0      0   100%
src/gui/dialogs/about_dialog.py              4      1    75%
src/gui/dialogs/fractal_type_dialog.py      28     20    29%
src/gui/dialogs/help_dialog.py              34     28    18%
src/gui/icons.py                            19      8    58%
src/gui/main_window.py                      23     13    43%
src/gui/menubar.py                          36     29    19%
src/icons/__init__.py                        0      0   100%
src/icons/application_exit.py                1      0   100%
src/icons/edit.py                            1      0   100%
src/icons/file_open.py                       1      0   100%
src/icons/file_save.py                       1      0   100%
src/icons/file_save_as.py                    1      0   100%
src/icons/fractal_new.py                     1      0   100%
src/icons/help_about.py                      1      0   100%
src/icons/help_faq.py                        1      0   100%
src/svitava-gui.py                           5      1    80%
------------------------------------------------------------
TOTAL                                      164    104    37%

S využitím přepínače –skip-covered je navíc možné vynechat ty soubory, jejichž všechny příkazy jsou alespoň jedenkrát vykonány:

$ coverage report --skip-covered
 
Name                                     Stmts   Miss  Cover
------------------------------------------------------------
src/gui/canvas.py                            7      4    43%
src/gui/dialogs/about_dialog.py              4      1    75%
src/gui/dialogs/fractal_type_dialog.py      28     20    29%
src/gui/dialogs/help_dialog.py              34     28    18%
src/gui/icons.py                            19      8    58%
src/gui/main_window.py                      23     13    43%
src/gui/menubar.py                          36     29    19%
src/svitava-gui.py                           5      1    80%
------------------------------------------------------------
TOTAL                                      164    104    37%
 
11 files skipped due to complete coverage.

11. Anotace zdrojového kódu

Příkazem:

$ coverage annotate

získáme výpis zdrojových kódů, ke kterým jsou přidány informace o tom, který z příkazů byl alespoň jedenkrát vykonán a který naopak vůbec vykonán nebyl. Výsledek pro náš demonstrační příklad s faktoriálem vypadá takto:

> """Výpočet faktoriálu."""
 
> def factorial(n):
>     """Rekurzivní výpočet faktoriálu."""
>     assert isinstance(n, int), "Integer expected"
 
>     if n < 0:
!         return None
>     if n == 0:
>         return 1
>     result = n * factorial(n-1)
 
>     assert isinstance(result, int), "Internal error in factorial computation"
>     return result
 
 
> def main():
>     for n in range(0, 11):
>         print(n, factorial(n))
 
 
> if __name__ == "__main__":
>     main()
Poznámka: povšimněte si, jak jsou odlišeny vykonané příkazy, nevykonané řádky a zbylé řádky.

12. Export výsledků do formátů JSON a XML

Nástroj coverage.py podporuje export naměřených výsledků, tedy přesněji řečeno informací o tom, které programové řádky byly vykonány a které nikoli, do formátů JSON a XML, což umožňuje relativně snadnou integraci tohoto nástroje například do integrovaných vývojových prostředí, nástrojů pro dynamickou analýzu zdrojových kódů atd.

Export do formátu JSON zajišťuje příkaz:

$ coverage json

Výsledkem bude (pro demonstrační příklad obsahující výpočet faktoriálu) tento soubor:

{"meta": {"version": "5.4", "timestamp": "2021-02-05T09:37:22.563357", "branch_coverage": false, "show_contexts": false}, "files": {"factorial.py": {"executed_lines": [1, 3, 5, 7, 9, 10, 11, 13, 14, 17, 18, 19, 22, 23], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [8], "excluded_lines": []}}, "totals": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "missing_lines": 1, "excluded_lines": 0}}

Který můžeme s využitím nástroje jq převést do čitelnější podoby, z níž je patrné jakým způsobem jsou informace prezentovány:

{
  "meta": {
    "version": "5.4",
    "timestamp": "2021-02-05T09:37:22.563357",
    "branch_coverage": false,
    "show_contexts": false
  },
  "files": {
    "factorial.py": {
      "executed_lines": [
        1,
        3,
        5,
        7,
        9,
        10,
        11,
        13,
        14,
        17,
        18,
        19,
        22,
        23
      ],
      "summary": {
        "covered_lines": 13,
        "num_statements": 14,
        "percent_covered": 92.85714285714286,
        "missing_lines": 1,
        "excluded_lines": 0
      },
      "missing_lines": [
        8
      ],
      "excluded_lines": []
    }
  },
  "totals": {
    "covered_lines": 13,
    "num_statements": 14,
    "percent_covered": 92.85714285714286,
    "missing_lines": 1,
    "excluded_lines": 0
  }
}

Podobný příkaz zajišťuje export do formátu XML:

$ coverge xml

Tentokrát je výsledný soubor větší, i když ve skutečnosti obsahuje podobné informace, pouze jinak prezentované:

<?xml version="1.0" ?>
<coverage version="5.4" timestamp="1612601186209" lines-valid="14" lines-covered="13" line-rate="0.9286" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
        <!-- Generated by coverage.py: https://coverage.readthedocs.io -->
        <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
        <sources>
                <source>/home/tester</source>
        </sources>
        <packages>
                <package name="." line-rate="0.9286" branch-rate="0" complexity="0">
                        <classes>
                                <class name="factorial.py" filename="factorial.py" complexity="0" line-rate="0.9286" branch-rate="0">
                                        <methods/>
                                        <lines>
                                                <line number="3" hits="1"/>
                                                <line number="5" hits="1"/>
                                                <line number="7" hits="1"/>
                                                <line number="8" hits="0"/>
                                                <line number="9" hits="1"/>
                                                <line number="10" hits="1"/>
                                                <line number="11" hits="1"/>
                                                <line number="13" hits="1"/>
                                                <line number="14" hits="1"/>
                                                <line number="17" hits="1"/>
                                                <line number="18" hits="1"/>
                                                <line number="19" hits="1"/>
                                                <line number="22" hits="1"/>
                                                <line number="23" hits="1"/>
                                        </lines>
                                </class>
                        </classes>
                </package>
        </packages>
</coverage>

13. Zobrazení výsledků ve formě vygenerovaných HTML stránek

Posledním způsobem zobrazení výsledků získaných nástrojem coverage.py je vygenerování HTML stránek s informacemi o tom, které části zdrojových kódů byly vykonány a které nikoli. Tento výstup získáme příkazem:

$ coverage html

Výsledkem je několik HTML stránek, CSS souborů a souborů s podpůrnými funkcemi JavaScriptu, které obsahují jak souhrnné informace o příkazech, které byly provedeny, tak i podrobnější informace vztažené k jednotlivým programovým řádků z každého zdrojového souboru.

Obrázek 3: Souhrnné informace o celém programu, který byl sledován.

Obrázek 4: Podrobné informace o příkazech provedených ve vybraném skriptu.

Obrázek 5: Souhrnné informace pro složitější projekt s více zdrojovými soubory.

14. Interní struktura s informacemi o trasování (SQLite databáze)

Pro úplnost se ještě podívejme na to, jaká data a v jakém formátu vlastně obsahuje soubor .coverage, který je vytvořen ve chvíli, kdy spustíme sledovaný program pomocí příkazu coverage run. Jedná se o binární soubor:

$ od -t x1 -N 1800 .coverage

Interní struktura nám mnoho neprozradí:

0000000 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
0000020 10 00 01 01 00 40 20 20 00 00 00 0a 00 00 00 0d
0000040 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 04
0000060 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00
0000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0a
0000140 00 2e 4b 90 0d 0f 56 00 0c 06 96 00 0f 5e 0d 58
0000160 0f 2d 0c a5 0d 2f 0b da 0c 76 09 93 0b a7 07 6a
0000200 09 6c 06 96 00 00 00 00 00 00 00 00 00 00 00 00
0000220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0003220 00 00 00 00 00 00 81 51 0c 07 17 19 19 01 82 7d
0003240 74 61 62 6c 65 74 72 61 63 65 72 74 72 61 63 65
0003260 72 0d 43 52 45 41 54 45 20 54 41 42 4c 45 20 74
0003300 72 61 63 65 72 20 28 0a 20 20 20 20 2d 2d 20 41
0003320 20 72 6f 77 20 70 65 72 20 66 69 6c 65 20 69 6e
0003340 64 69 63 61 74 69 6e 67 20 74 68 65 20 74 72 61
0003360 63 65 72 20 75 73 65 64 20 66 6f 72 20 74 68 61
0003400 74 20 66 69 6c 65 2e 0a
0003410

Ve skutečnosti lze poměrně snadno zjistit, že se jedná o soubor používající formát souborové databáze SQLite:

$ file .coverage
 
.coverage: SQLite 3.x database, last written using SQLite version 3034000

Můžeme tedy obsah tohoto souboru prozkoumat (ovšem pochopitelně s tím dodatkem, že se jeho formát může v dalších verzích změnit):

$ sqlite3 .coverage
 
SQLite version 3.34.0 2020-12-01 16:14:00
Enter ".help" for usage hints.

V databázi nalezneme celkem sedm tabulek:

sqlite> .tables
 
arc              coverage_schema  line_bits        tracer
context          file             meta

Důležité jsou především tabulky file, tracer a line_bits, mezi nimiž existují relační vazby:

sqlite> .schema file
 
CREATE TABLE file (
    -- A row per file measured.
    id integer primary key,
    path text,
    unique (path)
);
 
 
sqlite> .schema tracer
 
CREATE TABLE tracer (
    -- A row per file indicating the tracer used for that file.
    file_id integer primary key,
    tracer text,
    foreign key (file_id) references file (id)
);
 
 
sqlite> .schema line_bits
 
CREATE TABLE line_bits (
    -- If recording lines, a row per context per file executed.
    -- All of the line numbers for that file/context are in one numbits.
    file_id integer,            -- foreign key to `file`.
    context_id integer,         -- foreign key to `context`.
    numbits blob,               -- see the numbits functions in coverage.numbits
    foreign key (file_id) references file (id),
    foreign key (context_id) references context (id),
    unique (file_id, context_id)
);

Samozřejmě si můžeme obsah jednotlivých tabulek zobrazit:

sqlite> select * from file;
 
1|/home/tester/factorial.py

Ovšem v případě tabulky line_bits je nutné upozornit na to, že informace o řádcích s vykonanými příkazy jsou uloženy ve formě bitových množin popsaných na stránce https://coverage.readthedoc­s.io/en/coverage-5.3.1/dbschema.html#numbits (a to z důvodu větší efektivity uložení). Získání těchto informací pouze s využitím SQL příkazů je tedy dosti problematické.

15. Nástroj pycrunch-pytrace

Třetím nástrojem, se kterým se v dnešním článku setkáme, je nástroj nazvaný pycrunch-pytrace, popř. zkráceně pouze pytrace (což je ovšem nepřesné, protože existuje ještě jeden balíček s tímto jménem, který ovšem pracuje jinak). Tento potenciálně velmi užitečný nástroj dokáže sledovat a zaznamenávat činnosti prováděné sledovanou aplikací a následně zobrazit stav aplikace v libovolném čase do minulosti (a to až na úroveň volání jednotlivých funkcí a jejích parametrů). Jedná se tedy o jakousi obdobu debuggeru, v němž se ale můžeme pohybovat nikoli pouze dopředu (typicky příkazy step a run), ale i pozpátku. Toto zpětné „přetáčení“ minulosti aplikace je prováděno v grafickém uživatelském rozhraní běžícím ve webovém prohlížeči:

Obrázek 4: Grafické uživatelské rozhraní programu, kterým je možné zpětně sledovat chování aplikace.

16. Instalace nástroje pycrunch-pytrace

Instalace samotného nástroje pycrunch-pytrace (resp. přesněji řečeno jeho „trasovací“ části) je velmi jednoduchá, neboť se opět jedná o nástroj dostupný na PyPi. Instalaci provedeme následujícím způsobem:

$ pip install --user pycrunch-trace

Povšimněte si, že jednou ze závislostí tohoto nástroje je i Cython, což je technologie, kterou jsme se zabývali v článku RPython vs Cython aneb dvojí přístup k překladu Pythonu do nativního kódu:

Collecting pycrunch-trace
  Downloading https://files.pythonhosted.org/packages/ef/30/96a3666b1a88399183f8fad6fc930f325fdd16afad837063814005573c80/pycrunch-trace-0.1.5.tar.gz (44kB)
     |████████████████████████████████| 51kB 184kB/s
Collecting Cython
  Downloading https://files.pythonhosted.org/packages/9f/05/959e78f2aeade1c9e85a7adc4c376f454ecaeb4cb6b079ca7a85684b69c1/Cython-0.29.21-cp38-cp38-manylinux1_x86_64.whl (1.9MB)
     |████████████████████████████████| 1.9MB 339kB/s
Collecting jsonpickle
  Downloading https://files.pythonhosted.org/packages/77/a7/c2f527ddce3155ae9e008385963c2325cbfd52969f8b38efa2723e2af4af/jsonpickle-1.5.1-py2.py3-none-any.whl
Collecting PyYAML
  Downloading https://files.pythonhosted.org/packages/70/96/c7245e551b1cb496bfb95840ace55ca60f20d3d8e33d70faf8c78a976899/PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl (662kB)
     |████████████████████████████████| 665kB 4.5MB/s
Collecting protobuf==3.11.3
  Downloading https://files.pythonhosted.org/packages/9a/71/5cdb5ed762a537eac39097ae6ecf8785e276b5044efe99b8e53cb3addd7f/protobuf-3.11.3-cp38-cp38-manylinux1_x86_64.whl (1.3MB)
     |████████████████████████████████| 1.3MB 4.6MB/s
Requirement already satisfied: setuptools in /usr/lib/python3.8/site-packages (from protobuf==3.11.3->pycrunch-trace) (41.6.0)
Requirement already satisfied: six>=1.9 in /usr/lib/python3.8/site-packages (from protobuf==3.11.3->pycrunch-trace) (1.14.0)
Installing collected packages: Cython, jsonpickle, PyYAML, protobuf, pycrunch-trace
    Running setup.py install for pycrunch-trace ... done
Successfully installed Cython-0.29.21 PyYAML-5.4.1 jsonpickle-1.5.1 protobuf-3.11.3 pycrunch-trace-0.1.5

Základní otestování instalace můžeme provést interaktivně z REPLu jazyka Python:

$ python
 
Python 3.8.7 (default, Dec 22 2020, 00:00:00)
[GCC 10.2.1 20201125 (Red Hat 10.2.1-9)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pycrunch_trace
>>> help(pycrunch_trace)
 
Help on package pycrunch_trace:
 
NAME
    pycrunch_trace
 
PACKAGE CONTENTS
    client (package)
    config
    demo (package)
    events (package)
    ...
    ...
    ...

17. Úprava trasovaného skriptu a spuštění trasování

Na rozdíl od obou předchozích nástrojů vyžaduje pycrunch-trace úpravu zdrojových kódů sledované aplikace. Ve skutečnosti je ovšem tato úprava triviální, protože je zapotřebí přidat jeden příkaz import:

from pycrunch_trace.client.api import trace

… a následně použít anotaci @trace u té funkce, u níž má trasování začít. Může se jednat například o funkci main:

from pycrunch_trace.client.api import trace
 
"""Výpočet faktoriálu."""
 
def factorial(n):
    """Rekurzivní výpočet faktoriálu."""
    assert isinstance(n, int), "Integer expected"
 
    if n < 0:
        return None
    if n == 0:
        return 1
    result = n * factorial(n-1)
 
    assert isinstance(result, int), "Internal error in factorial computation"
    return result
 
 
@trace
def main():
    for n in range(0, 11):
        print(n, factorial(n))
 
 
if __name__ == "__main__":
    main()

Ovšem tuto anotaci můžeme přidat k libovolné funkci, která se interně volá:

from pycrunch_trace.client.api import trace
 
"""Výpočet faktoriálu."""
 
@trace
def factorial(n):
    """Rekurzivní výpočet faktoriálu."""
    assert isinstance(n, int), "Integer expected"
 
    if n < 0:
        return None
    if n == 0:
        return 1
    result = n * factorial(n-1)
 
    assert isinstance(result, int), "Internal error in factorial computation"
    return result
 
 
def main():
    for n in range(0, 11):
        print(n, factorial(n))
 
 
if __name__ == "__main__":
    main()

Následně je již možné sledovanou aplikaci spustit, a to naprosto běžným způsobem (což je další rozdíl oproti nástrojům trace a coverage):

$ python factorial.py

Sledovaný program by se měl spustit kromě dalších informací vypsat i zprávy o tom, že bylo zahájeno sledování:

/home/tester/.local/lib/python3.8/site-packages/Cython/Compiler/Main.py:369: FutureWarning: Cython directive 'language_level' not set, using 2 for now (Py2). This will change in a later release! File: /home/tester/.local/lib/python3.8/site-packages/pycrunch_trace/client/networking/strategies/native_write_strategy.pyx
  tree = Parsing.p_module(s, pxd, full_module_name)
 
----print_timings----
tracing complete, saving results
total_samples - 538
total overhead time - 6 ms
                      6.324903004497173
0.01176 ms avg call time overhead
total events C: 31
main - put_events: so far: 531
finalizing native tracer
queue length 0
got evt EventsSlice
put_file_slice
tracing_did_complete
queue length 0
got evt FileContentSlice
skip_to_free_header_chunk
pos = 12
after pos = 28
queue length 0
got evt StopCommand
skip_to_free_header_chunk
pos = 12
after pos = 52
metadata saved to /home/tester/pycrunch-recordings/main/pycrunch-trace.meta.json
Timeout while waiting for new msg... Thread will stop for now
Thread stopped

Výsledkem by měl být soubor pycrunch-trace.meta.json a především pak binární soubor session.chunked.pycrunch-trace, který použijeme v navazující kapitole.

18. Prohlédnutí výsledků trasování v GUI

Soubor session.chunked.pycrunch-trace je možné nahrát do webové aplikace dostupné na adrese https://pytrace.com/ (popř. si tuto aplikaci spustit lokálně – existuje v repositáři https://github.com/gleb-sevruk/pycrunch-tracing-webui). Měla by se objevit především časová osa, po které je možné se pohybovat a zobrazit tak stav programu ve vybraném okamžiku. V dolní části je navíc možné zapnout pseudografické zobrazení volaných funkcí:

Obrázek 7: Načtení souboru s informacemi o sledované aplikaci se zobrazením právě prováděného příkazu.

Obrázek 8: V pravé části jsou zobrazeny informace o parametrech i o lokálních proměnných.

Obrázek 9: Grafické znázornění rekurzivního výpočtu faktoriálu.

Obrázek 10: Krokování volanými funkcemi (opět se zobrazují i parametry a lokální proměnné).

Obrázek 11: Odlišná aplikace s GUI.

Obrázek 12: Vzorová aplikace určená pro otestování možností pycrunch-trace.

Hacking tip

19. Repositář s demonstračními příklady

Zdrojové kódy všech tří dnes použitých demonstračních příkladů určených pro Python 3 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs (jedná se o několik variant implementace funkce pro výpočet faktoriálu). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Demonstrační příklad Stručný popis příkladu Cesta
1 factorial.py původní varianta skriptu pro výpočet faktoriálu https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/factorial.py
2 factorial_pycrunch1.py úprava pro trasování s využitím pycrunch-trace https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial_pycrunch1.py
3 factorial_pycrunch2.py alternativní úprava pro trasování s využitím pycrunch-trace https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial_pycrunch2.py

K dispozici jsou i datové soubory vytvořené nástrojem coverage.py i standardním nástrojem trace:

# Demonstrační příklad Stručný popis příkladu Cesta
1 factorial.cover počet přístupů k jednotlivým řádkům kódu https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial.cover
2 factorial.trace výsledek trasování https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial.trace
3 factorial.timing výsledek trasování + informace o relativním času běhu aplikace https://github.com/tisnik/most-popular-python-libs/blob/master/tracing/fac­torial.timing

20. Odkazy na Internetu

  1. Stránka projektu PyTrace
    https://pytrace.com/
  2. Nástroj pycrunch-trace na PyPi
    https://pypi.org/project/pycrunch-trace/
  3. Repositář nástroje pycrunch-trace na GitHubu
    https://github.com/gleb-sevruk/pycrunch-trace
  4. Repositář serveru pycrunch-tracing-webui na GitHubu
    https://github.com/gleb-sevruk/pycrunch-tracing-webui
  5. Server s GUI pro nástroj pycrunch-trace
    https://app.pytrace.com/
  6. Dokumentace k projektu Coverage.py
    https://coverage.readthedoc­s.io/en/coverage-5.3.1/index.html
  7. Projekt coveragepy na GitHubu
    https://github.com/nedbat/coveragepy
  8. Projekt coverage 5.4 na PyPi
    https://pypi.org/project/coverage/
  9. Coverage.py database schema
    https://coverage.readthedoc­s.io/en/coverage-5.3.1/dbschema.html
  10. Numbits
    https://coverage.readthedoc­s.io/en/coverage-5.3.1/dbschema.html#numbits
  11. SQLite Show Tables
    https://www.sqlitetutorial.net/sqlite-tutorial/sqlite-show-tables/
  12. SQLite Describe Table
    https://www.sqlitetutorial.net/sqlite-tutorial/sqlite-describe-table/
  13. Trasování a ladění nativních aplikací v Linuxu
    https://www.root.cz/clanky/trasovani-a-ladeni-nativnich-aplikaci-v-linuxu/
  14. Trasování a ladění nativních aplikací v Linuxu: použití GDB a jeho nadstaveb
    https://www.root.cz/clanky/trasovani-a-ladeni-nativnich-aplikaci-v-linuxu-pouziti-gdb-a-jeho-nadstaveb/
  15. Trasování a ladění nativních aplikací v Linuxu: pokročilejší možnosti nabízené GNU Debuggerem
    https://www.root.cz/clanky/trasovani-a-ladeni-nativnich-aplikaci-v-linuxu-pokrocilejsi-moznosti-nabizene-gnu-debuggerem/
  16. Trasování a ladění nativních aplikací v Linuxu: nástroj SystemTap
    https://www.root.cz/clanky/trasovani-a-ladeni-nativnich-aplikaci-v-linuxu-pouziti-nastroje-systemtap/
  17. Trasování a ladění v Linuxu: jazyk používaný SystemTapem
    https://www.root.cz/clanky/trasovani-a-ladeni-v-linuxu-jazyk-pouzivany-systemtapem/
  18. Grafická nadstavba nad GNU Debuggerem gdbgui a její alternativy
    https://www.root.cz/clanky/graficka-nadstavba-nad-gnu-debuggerem-gdbgui-a-jeji-alternativy/
  19. Debugging C128 BASIC with TRON, TRAP, and More
    https://www.youtube.com/wat­ch?v=D11AuAl5T-s
  20. TRON command
    https://en.wikipedia.org/wi­ki/TRON_command