Что такое график вызовов? И как их автоматически генерировать

Вы когда-нибудь смотрели на строки кода, пытаясь представить себе, как все разные функции сочетаются и взаимодействуют друг с другом?

Это может быть сложной задачей, особенно в больших и более сложных программах.

Но не бойтесь! Существует способ визуализировать поток вызовов и понять это все: график вызовов.

Представьте, что вы видите диаграмму, которая показывает, как каждая функция вписывается в общую картину и как они обращаются друг к другу для выполнения своей работы. Звучит как мечта, верно?

Ну это не просто мечта. Посредством правильных инструментов и методов вы можете автоматизировать создание графика вызовов и гораздо лучше понять свою кодовую базу.

В этой статье мы представим вам, как это сделать. Мы рассмотрим как статический, так и динамический подход к созданию графиков вызовов и обсудим плюсы и минусы каждого из них.

Независимо от того, вы опытный разработчик или только начинаете, в этом руководстве вы найдете ценную информацию и информацию о том, как автоматизировать создание графика вызовов. Итак, берите свой любимый текстовый редактор и начинайте!

В этой статье мы рассмотрим:

  1. Что такое график вызовов?
  2. Динамические и статические графики вызовов
  3. Зачем нужны графы вызовов?
  4. Автоматическая генерация графика вызовов
  5. Генерация динамического графика вызовов
  6. Вывод

Что такое график вызовов?

График вызовов – это графическое представление связей между разными вызовами функций в программе. Он показывает, как функции в программе взаимодействуют друг с другом, позволяя разработчикам понять поток программы и определить потенциальные проблемы с производительностью.

Графики вызовов можно сделать вручную, но это может быть изнурительным и трудоемким действием, в особенности для больших программных проектов и программ.

Вот где на помощь приходит автоматизация. Автоматизировать создание графиков вызовов, разработчики могут сэкономить время и усилия и сосредоточиться на более важных задачах.

Существует два главных подхода к автоматизации сотворения графа вызовов: статический анализ и динамический анализ. Статический анализ предполагает анализ исходного кода программы без ее выполнения, в то время как динамический анализ предполагает запуск программы и анализ ее поведения при выполнении.

Оба подхода имеют свои преимущества и недостатки, и самый подходящий подход для определенной ситуации будет зависеть от конкретных потребностей и целей разработчика.

В этой статье мы подробнее рассмотрим оба подхода и обсудим, как выбрать правильный для ваших нужд.

Независимо от того, какой подход вы выберете, автоматизация создания графика вызовов может стать мощным инструментом повышения эффективности и результативности процесса разработки программного обеспечения.

пример
Изображение автора

Например, на изображении выше показано, как выглядел бы граф вызовов некоторого простого кода Java, целью которого является выполнение одной из 3 возможных подпрограмм с входами и выходами.

import java.util.Scanner;

class Ejercicio3 extends Practicas {
    public String name = "3---Inversor dígitos";
    public String description = "Devuelve el número con los dígitos invertidos";

    public void mainExec() {
        showDescription(name, description);
        
        int b = in.nextInt();
        int h = in.nextInt();
        System.out.println(b*h/2);
    }
}

class Ejercicio2 extends Practicas {
    public String name = "2---Inversor dígitos";
    public String description = "Devuelve el número con los dígitos invertidos";

    public void mainExec() {
        showDescription(name, description);
        
        int input = in.nextInt();
        String temp = ""+input;
        String out = "";
        for(int i=0;i<temp.length();i++){
            out += temp.charAt(temp.length()-i-1);
        }
        int out1 = Integer.parseInt(out);
        System.out.println(out1);
    }
}

class Ejercicio1 extends Practicas {
    public String name = "1---Programa Hola mundo";
    public String description = "Simplemente Hola mundo";

    public void mainExec() {
        showDescription(name, description);
        System.out.println("Hello world!");
    }
}

public class Practicas {  
    Scanner in = new Scanner(System.in);  

    public void showDescription(String name, String description) {
        System.out.println(String.format("Nombre: %s \nDescripción: %s\nResultado de ejecución:\n", name, description));
    }

    public static void main(String args[]) {
           Ejercicio1 ej1 = new Ejercicio1();
           ej1.mainExec();

           Ejercicio2 ej2 = new Ejercicio2();
           ej2.mainExec();

           Ejercicio3 ej3 = new Ejercicio3();
           ej3.mainExec();
    }
}

Как вы можете видеть внутри кода, каждая подпрограмма сохраняется в классе, вызванном непосредственно из main() метод.

Динамические и статические графики вызовов

Динамический график вызовов – это представление потока управления в программе во время ее выполнения. Он показывает последовательность вызовов функций, осуществляемых при выполнении программы, а также параметры, передаваемые каждой функции.

В отличие от этого, статический граф вызовов – это представление потока управления в программе, созданного на основе исходного кода программы без учета фактического выполнения программы.

Одним из главных отличий между динамическими и статическими графиками вызовов является обеспечивающий уровень детализации.

Динамический график вызовов показывает точную последовательность вызовов функций, сделанных при выполнении программы, вместе с конкретными параметрами, которые были переданы каждой функции.

Это может быть очень полезным для отладки и оптимизации кода, поскольку позволяет разработчикам точно видеть, как выполняется программа, и определять какие-либо потенциальные узкие места или неэффективность.

В отличие от этого, статический график вызовов основан на исходном коде программы и не учитывает никакого фактического выполнения программы. Как следствие, статический график вызовов не предоставляет столько деталей, сколько динамический график вызовов, и может точно отображать фактический поток управления в программе.

Но статические графы вызовов могут быть очень полезны для понимания общей структуры программы и определения потенциальных зависимостей между разными частями кода.

Еще одно отличие между динамическими и статическими графами вызовов состоит в способе их построения.

Динамические графики вызовов генерируются при выполнении программы, поэтому для создания они требуют выполнения программы. Это может занять много времени, особенно для больших программ, и может потребовать многократного запуска программы, чтобы охватить все возможные пути выполнения.

В отличие от этого, статические графики вызовов могут быть сгенерированы непосредственно из исходного кода программы без необходимости фактического запуска программы. Это может быть гораздо более быстрым и эффективным, особенно для больших программ, и позволяет разработчикам анализировать структуру программы, не дожидаясь ее выполнения.

Есть также некоторые ограничения для динамических и статических графов вызовов. Динамические графики вызовов могут только предоставлять информацию о фактическом выполнении программы и не учитывать никаких потенциальных путей выполнения, которые не были использованы. Это может осложнить выявление определенных типов ошибок или неэффективности, которые могут возникнуть только при определенных условиях.

Кроме того, статические графики вызовов могут только предоставлять информацию о потенциальных путях выполнения программы и не учитывать фактическое выполнение программы. Это может осложнить точное отображение фактического потока управления в программе, особенно если программа имеет сложный поток управления или использует расширенные функции языка, такие как рекурсивные функции или обработка исключений.

В целом динамические и статические графики вызовов являются полезными инструментами для понимания потока управления в программе. Вы можете использовать их вместе, чтобы получить более полное представление о том, как выполняется приложение.

Динамические графики вызовов предоставляют подробную информацию о фактическом выполнении программы, в то время как статические графики вызовов предоставляют более абстрактное представление о потенциальных путях выполнения программы.

Оба могут быть очень полезны для настройки, оптимизации и поддержки кода, и вы можете использовать их в разных контекстах, чтобы помочь себе лучше понять и улучшить свой код.

Зачем нам нужны графики вызовов?

Основные преимущества использования графиков вызовов, как мы видели в предыдущих разделах, можно подытожить следующими понятиями:

  1. Понимание кодовой базы
  2. Настройка
  3. Оптимизация производительности
  4. Рефакторинг

Визуализируя поток управления через код, мы можем увидеть, как разные части кода взаимодействуют друг с другом и как они вписываются в общую картину. Это может быть особенно полезно при работе над новой для вас кодовой базой, поскольку это может дать вам общий обзор того, как все сочетается.

Графики вызовов также могут быть полезны для настройки. Визуализируя поток управления через код, разработчики могут выявить потенциальные проблемы или ошибки, такие как бесконечные циклы или рекурсия. Это может быть особенно полезным при работе над кодовой базой, склонной к ошибкам, или когда вы пытаетесь исправить сложную проблему.

Они также улучшают оптимизацию производительности, отображая, какие функции или методы часто вызываются, а какие могут вызвать задержки. Используя эту информацию, мы можем обнаружить утечки производительности и принять меры для оптимизации кода.

Это может быть особенно важно в приложениях, которые должны быть быстрыми и отзывчивыми, например, в системах реального времени или мобильных приложениях.

Наконец, мы можем использовать преимущества графов вызовов при рефакторинге кода. Чтобы визуализировать, какие функции или методы зависят от меняющегося кода, мы можем увидеть влияние их изменений и убедиться, что они ничего не нарушают.

Это может быть особенно важно при работе над кодовой базой, которая имеет решающее значение для операций организации, поскольку это может помочь предотвратить дорогие простои.

Автоматизация создания графиков вызовов полезна, поскольку она может сэкономить время и усилия разработчика. Вместо того, чтобы вручную чертить график вызовов, вы можете использовать инструмент для автоматического создания графика на основе кода. Это может быть особенно полезно в больших кодовых базах, где ручное создание графика вызовов может занять много времени.

Кроме того, автоматическое создание графика вызовов помогает убедиться, что график вызовов точен и актуален, поскольку он будет базироваться на новейшей версии кода. Это становится особенно важным в постоянно развивающихся кодовых базах, поскольку это может помочь гарантировать, что граф вызовов остается актуальным и правильным.

Короче говоря, графики вызовов могут быть невероятно полезны для разработчиков, работающих над программным проектом. Они помогают разработчикам понять кодовую базу, устранять проблемы, оптимизировать производительность и рефакторировать код.

Автоматизация создания графиков вызовов может сэкономить время и усилия разработчика и обеспечить точность и значимость графика вызовов.

Автоматическая генерация графика вызовов

Первый метод, который мы рассмотрим, — это создание статического графа вызовов в Java, поскольку существуют такие редакторы кода, как Intellij Ultimate предлагающие инструменты и плагины для визуализации такого графика без необходимости добавлять дополнительные строки кода.

Итак, открыв редактор, нажмите Ctrl+Alt+S чтобы получить доступ к настройкам редактора, или просто перейдите к Файл->Настройки меню. Затем войдите в раздел Plugins и найдите плагин Call Graph.

А
Изображение автора

Как вы можете видеть в его описании, его целью является визуализация Java (Поддерживается только язык) графы вызовов самым простым возможным способом, которым мы можем воспользоваться.

После установки плагина вы можете начать создавать собственные статические графики вызовов, перейдя в Просмотр->Окно инструментов->График вызовов. Если он не отображается в верхнем меню, возможно, вам придется перезапустить редактор.

Без названия
Изображение автора

Наконец, теперь можно легко визуализировать свой поток кода и связи между внутренними компонентами, как вы можете видеть в приведенном выше примере, взятом из этого проекта, который мы также будем использовать в будущих случаях.

Продолжая статические графы вызовов, мы сосредоточимся на конкретном методе, подходящем для языка программирования Python. Итак, мы начнем с установки необходимых модулей для выполнения процесса:

!pip install pyvis
!pip install pycg

С поп команду мы установили pyvis, который обеспечивает простой и интуитивно понятный интерфейс создания, визуализации и анализа сетей. Кроме того, мы включили pycg для получения информации о формате графика нужного файла сценария Python.

В этом случае мы используем в качестве простого первоначального примера сценарий, содержащий только a Привет Мир Программа. Однако вы можете использовать любое нужное приложение, независимо от сложности и расширения:

print("hello world")

Когда все будет готово к запуску, первым шагом будет получение данных графика вызовов с помощью pycg библиотека. С помощью такой команды мы сохраняем в файле .json всю необходимую информацию о нашем статическом графике вызовов, который позже будет преобразован в его визуальное представление:

!pycg file.py -o cg.json

Вторым шагом является визуализация полученного графика из файла .json. Следовательно, с pyvis и json Модули Python, мы можем превратить наши текущие данные формата JSON в HTML файл, отображающий интерактивную версию полученного графика.

import networkx as nx
from pyvis.network import Network
import json

def toNetwork(data: dict)->  nx.DiGraph:
    nt = nx.DiGraph()

    def checkKey(name):
        if name not in nt:
            nt.add_node(name, size=40)

    for node in data:
        checkKey(node)
        for child in data[node]:
            checkKey(child)
            nt.add_edge(node,child)
    return nt

def ntw_pyvis(ntx:nx.DiGraph):
    net = Network(width="1000px",height="1000px", directed=True)
    for node in ntx.nodes:
        net.add_node(node, **{"label":node},)

    for edge in ntx.edges:
        net.add_edge(edge[0], edge[1], width=1)
    net.show('graph.html')

with open("cg.json","r") as f:
    data = json.load(f)

ntw_pyvis(toNetwork(data))
рж
Изображение автора

Для простого приложения hello world это будет статический график вызовов. Как вы видите, есть выходной узел (file.py) и приемный узел, инкапсулирующий уникальную функцию, присутствующую в нашей программе, встроенную print().

Без названия2-2
Изображение автора

Однако не все статические графы вызовов должны иметь источник и приемник, поскольку они могут вызвать любое количество функций, классов или других модулей.

К примеру, приведенный выше график показывает более сложный алгоритм, главной функцией которого является разумная игра в игру Wordle. Обратите внимание на количество встроенных функций, задействованных в нем, а также на ссылку между функциями сценария. Все это доказывает, что граф не всегда имеет фиксированную структуру – это зависит от качества написанного кода.

Генерация динамического графика вызовов

В этом последнем разделе мы научимся автоматически создавать динамический график вызовов для мониторинга процессов Python.

В отличие от предыдущего метода, нам нужен только один модуль для создания графика, а общий дополнительный код, необходимый для процесса, значительно проще.

!pip install pycallgraph

Когда мы установили pycallgraph библиотека, которая, как свидетельствует ее название, будет отвечать за создание и визуализацию динамического графика вызовов, связанного с нашим кодом.

Мы можем импортировать его в новый сценарий Python и использовать PyCallGraph/GraphvizOutput объектов для создания файла .png с соответствующим графом вызовов.

from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput

graph = GraphvizOutput()
graph.output_file = "file4.png"

with PyCallGraph(output=graph):
  print("Hello world")
файл4
Изображение автора

Для простого приложения hello world, запущенного из Google Colab, можно заметить, что структура графика теперь зависит от процесса, с помощью которого запускался наш код.

Следовательно, если функция не достигнута потоком выполнения, она не будет показана на динамическом графике, в то время как на статическом графике она будет показана. Это одна из наиболее трансцендентных причин, почему мы делаем разницу между обоими типами.

import re
import math
import json
import os
import concurrent
from concurrent.futures import ProcessPoolExecutor
import requests
import numpy as np

from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput

def procesarEntrada():
    entrada = []
    while len(entrada) != 5:
        entrada = [int(a) for a in input()[:5] if int(a) in range(0, 3)]
    return entrada


def generatePattern(entrada, word):
    pattern = ""
    procesed = {}

    for j in range(len(entrada)):
        letra = word[j]
        if letra not in procesed:
            condition = [k for k in range(j + 1, len(entrada)) if word[k] == letra and entrada[k] == 2]
            if entrada[j] == 0:
                if condition == []: procesed[letra] = 0
            else:
                procesed[letra] = 0
            pattern += [f"(?=[^{letra}]*$)" if condition == [] else f"(?!.{{{j}}}{letra})",
                        f"(?!.{{{j}}}{letra})(?=.*{letra})" + "".join(f"(?!.{{{i}}}{letra})" for i in [k for k in range(j + 1, len(entrada)) if word[k] == letra and entrada[k] in [0, 1]]),
                        f"(?=.{{{j}}}{letra})" + "".join(f"(?!.{{{i}}}{letra})" for i in [k for k in range(j + 1, len(entrada)) if word[k] == letra and entrada[k] in [0, 1]])][
                entrada[j]]
    return f"^{pattern}.*$"


def scoreWord(word, d):
    combinations = ["00000", "00001", "00002", "00010", "00011", "00012", "00020", "00021", "00022", "00100", "00101",
                    "00102", "00110", "00111", "00112", "00120", "00121", "00122", "00200", "00201", "00202", "00210",
                    "00211", "00212", "00220", "00221", "00222", "01000", "01001", "01002", "01010", "01011", "01012",
                    "01020", "01021", "01022", "01100", "01101", "01102", "01110", "01111", "01112", "01120", "01121",
                    "01122", "01200", "01201", "01202", "01210", "01211", "01212", "01220", "01221", "01222", "02000",
                    "02001", "02002", "02010", "02011", "02012", "02020", "02021", "02022", "02100", "02101", "02102",
                    "02110", "02111", "02112", "02120", "02121", "02122", "02200", "02201", "02202", "02210", "02211",
                    "02212", "02220", "02221", "02222", "10000", "10001", "10002", "10010", "10011", "10012", "10020",
                    "10021", "10022", "10100", "10101", "10102", "10110", "10111", "10112", "10120", "10121", "10122",
                    "10200", "10201", "10202", "10210", "10211", "10212", "10220", "10221", "10222", "11000", "11001",
                    "11002", "11010", "11011", "11012", "11020", "11021", "11022", "11100", "11101", "11102", "11110",
                    "11111", "11112", "11120", "11121", "11122", "11200", "11201", "11202", "11210", "11211", "11212",
                    "11220", "11221", "11222", "12000", "12001", "12002", "12010", "12011", "12012", "12020", "12021",
                    "12022", "12100", "12101", "12102", "12110", "12111", "12112", "12120", "12121", "12122", "12200",
                    "12201", "12202", "12210", "12211", "12212", "12220", "12221", "12222", "20000", "20001", "20002",
                    "20010", "20011", "20012", "20020", "20021", "20022", "20100", "20101", "20102", "20110", "20111",
                    "20112", "20120", "20121", "20122", "20200", "20201", "20202", "20210", "20211", "20212", "20220",
                    "20221", "20222", "21000", "21001", "21002", "21010", "21011", "21012", "21020", "21021", "21022",
                    "21100", "21101", "21102", "21110", "21111", "21112", "21120", "21121", "21122", "21200", "21201",
                    "21202", "21210", "21211", "21212", "21220", "21221", "21222", "22000", "22001", "22002", "22010",
                    "22011", "22012", "22020", "22021", "22022", "22100", "22101", "22102", "22110", "22111", "22112",
                    "22120", "22121", "22122", "22200", "22201", "22202", "22210", "22211", "22212", "22220", "22221",
                    "22222"]
    finalScore = 0

    for c in combinations:
        entrada = [int(i) for i in c]
        pattern = generatePattern(entrada, word)
        p = 0
        for i in d.keys(): p += 1 if re.match(pattern, i) else 0
        p /= len(d)
        finalScore += p * math.log(p, 2) if p > 0 else 0
    # print(f"{word}:{finalScore}")
    return finalScore


def paralelDict(item, d):
    return {i: scoreWord(i, d) for i in item}


def updateDict(d, pattern):
    d = {k: 0 for (k, v) in d.items() if re.match(pattern, k)}

    n = os.cpu_count()
    chunkSize = math.ceil(len(d) / n)
    out = {}
    with ProcessPoolExecutor(n) as executor:
        futures = [executor.submit(paralelDict, list(d.keys())[chunkSize * i:chunkSize * (i + 1)], d) for i in range(n)]
        for future in concurrent.futures.as_completed(futures):
            out.update(future.result())
        executor.shutdown()
    return out


def validarEntrada(entrada, word, globalPattern):
    procesed = {}
    for i in range(len(entrada)):
        letra = word[i]
        if letra not in procesed:
            if entrada[i] == 0:
                if (f"(?=.{{{i}}}{letra})" in globalPattern) or (f"(?=.*{letra})" in globalPattern and not max(
                        [entrada[j] == 2 and word[j] == letra for j in range(i + 1, len(entrada))] + [False])) or max([entrada[j] == 1 and word[j] == letra for j in range(i + 1, len(entrada))] + [False]):
                    print(f"Error en 0 letra {letra}")
                    return False
            elif entrada[i] == 1:
                if f"(?=.{{{i}}}{letra})" in globalPattern:
                    return False
            elif entrada[i] == 2:
                if f"(?!.{{{i}}}{letra})" in globalPattern or f"(?=[^{letra}]*$)" in globalPattern:
                    print(f"Error en 2 letra {letra}")
                    return False
            procesed[letra] = 0
    return True

if __name__ == '__main__':
  graph = GraphvizOutput()
  graph.output_file = "file3.png"

  with PyCallGraph(output=graph):
    intentos = 6
    d = json.loads(requests.get("
    globalPattern = ""
    for intento in range(intentos):
        print(len(d), d, len(d))
        word = max(d, key=d.get)
        print(word, d[word])

        entrada = procesarEntrada()
        pattern = generatePattern(entrada, word)
        if validarEntrada(entrada, word, globalPattern):
            globalPattern += pattern[1:-3]
            try:
              d=json.loads(requests.get(f"https://media.githubusercontent.com/media/cardstdani/practica-java/main/Data/MaxTree/Dict{intento+1}-{''.join([str(a) for a in entrada])}.txt").text)
            except:
              d = updateDict(d, pattern)
        else:
            print("Error detectado, entrada inconsistente")
            intento -= 1
        if entrada == [2, 2, 2, 2, 2]:
            break

Чтобы завершить этот раздел, мы сгенерируем динамический график вызовов более сложного кода, подобного вышеприведенному, который является алгоритмом Wordle Solver, упомянутым ранее.

Если мы запустим код с персонального компьютера, мы получим следующий результат:

файл
Изображение автора

Вы можете наблюдать, как в каждом узле есть некоторые данные о названии функции, вызовах в течение жизни процесса и времени. «живой» во время исполнения. Кроме того, число вызовов можно определить по степени каждого узла, который отображается внутри каждого направленного ребра.

файл2
Изображение автора

Однако, если мы запустим код из Google Colab, мы столкнемся с гораздо более сложным графом вызовов, следствием всех подпроцессов, которые необходимо выполнить для связи с удаленной машиной, которую предлагают службы Google, а также данных нескольких серверов подпрограммы поиска и многопроцессорной обработки. которых нужен сам код.

Вывод

С этим руководством вы узнали, как автоматизировать создание графиков вызовов, которые сами по себе могут казаться не столь ценными, как на самом деле.

Но они могут стать важными в ситуациях, когда мы должны оптимизировать алгоритмы настолько велики, что чтение их кода окажется неэффективным по сравнению с использованием графического представления.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *