Monitorando

O processo de monitoramento é um tópico relativamente complexo por ter diferentes casos de uso e uma boa diversidade de variações.

Este guia apresenta alguns dos casos mais comuns e possivelmente que mais se destacam. São propostas que podem lhe dar algumas dicas/idéias de como implementar o monitoramento de uma simulação e adaptá-la a sua necessidade.

Porém, antes de começar é preciso definir:

O que você deseja monitorar?

Quando você quer monitorar?

  • Em intervalos regulares?
  • Quando algo ocorrer?

Como pretende armazenar os dados coletados?

  • Em uma lista simples?
  • Registrar em um arquivo?
  • Armazenar em uma base de dados?

As seções a seguir analizam estas perguntas e provêem alguns exemplos para lhe ajudar.

Monitorando seus processos

Monitorar seus proprios processos é uma coisa relativamente simples, afinal você controla o código. Temos conhecimento que a coisa mais comum que se deseja fazer é monitorar o valor de uma ou mais variáveis de estado toda vez que elas mudarem ou em um intervalo de tempo e aramazenar essa informação em algum lugar ( na memória, em um banco de dados ou até mesmo um arquivo, por exemplo).

Um exemplo simples seria o uso de uma lista e adicionar os valores desejados para análse a cada vez que ela mudasse:

>>> import simpy
>>>
>>> data = []  # Esta lista armazenara os dados coletados
>>>
>>> def test_process(env, data):
...     val = 0
...     for i in range(5):
...         val += env.now
...         data.append(val)  # Armazenando o dado
...         yield env.timeout(1)
>>>
>>> env = simpy.Environment()
>>> p = env.process(test_process(env, data))
>>> env.run(p)
>>> print('Collected', data)  # Imprimindo os dados coletados
Collected [0, 1, 3, 6, 10]

Se precisa monitorar múltiplas variáveis, você pode inserir tuplas em sua lista.

Caso queira armazenar os dados em um array Numpy ou uma base de dados, muitas vezes você pode melhorar o desempenho se você guardar os dados em uma lista no Python e só escrever as partes maiores ( ou o dataset completo ) em uma base de dados.

Uso de recursos

São muitos os casos de uso para monitoramento de recursos, por exemplo você pode desejar monitorar:

  • O uso de um recurso no decorrer da simulação e ter média de uso, que seria,

    • o número de processos que usaram o recurso no decorrer da simulação
    • o nível de um contêiner
    • a quantidade de itens em uma loja

    Isto pode ser monitorado também no decorrer do tempo da simulação discreta ou a cada vez que houver uma mudança.

  • O número de processos armazenados ou retirados de uma fila no decorrer da simulação ( e a quantidade média ). Mais uma vez, isto pode ser monitorado no decorrer do tempo da simulação ou a cada mudança.

  • Para Recursos Preemptivos, você pode desejar medir a frequência com que a preempção ocorre no decorrer do tempo da simulação.

Ao contrário do que ocorre nos processos, você não tem acesso direto ao código das classes de recurso embutidas. Porém isso não o impede de monitorá-las.

Algumas adaptações nos métodos dos recursos permitem que você capture todos os dados necessários.

Segue um exemplo que demonstra como você pode adicionar retornos para um recurso que deverá ser chamado apenas antes ou depois de um evento get / request ou um put / release:

>>> from functools import partial, wraps
>>> import simpy
>>>
>>> def patch_resource(resource, pre=None, post=None):
...     """Ajuste Patch *resource* que executa o metodo *pre* antes de cada
...     operacao de put/get/request/release e o metodo *post* executado apos
...     cada operacao. O unico argumento solicitado para essas funcoes eh a
...     instancia do recurso.
...
...     """
...     def get_wrapper(func):
...         # Cria um wrapper para put/get/request/release
...         @wraps(func)
...         def wrapper(*args, **kwargs):
...             # Este wrapper executa a
...             # funcao "pre"
...             if pre:
...                 pre(resource)
...
...             # Executa a operacao atual
...             ret = func(*args, **kwargs)
...
...             # Chama a funcao "post"
...             if post:
...                 post(resource)
...
...             return ret
...         return wrapper
...
...     # Substitui as operacoes originais com nosso wrapper
...     for name in ['put', 'get', 'request', 'release']:
...         if hasattr(resource, name):
...             setattr(resource, name, get_wrapper(getattr(resource, name)))
>>>
>>> def monitor(data, resource):
...     """Esta eh a funcao de monitoramento."""
...     item = (
...         resource._env.now,  # O tempo na simulacao atual
...         resource.count,  # O numero de usuarios
...         len(resource.queue),  # O numero de processos enfileirados
...     )
...     data.append(item)
>>>
>>> def test_process(env, res):
...     with res.request() as req:
...         yield req
...         yield env.timeout(1)
>>>
>>> env = simpy.Environment()
>>>
>>> res = simpy.Resource(env, capacity=1)
>>> data = []
>>> # Vincular *data* como o primeiro argumento de monitor()
>>> # veja mais em https://docs.python.org/3/library/functools.html#functools.partial
>>> monitor = partial(monitor, data)
>>> patch_resource(res, post=monitor)  # Altera/Modifica (somente) esta instancia do recurso
>>>
>>> p = env.process(test_process(env, res))
>>> env.run(p)
>>>
>>> print(data)
[(0, 1, 0), (1, 0, 0)]

O exemplo acima é bem genérico mas também é uma forma bem flexível de monitorar todos os aspectos de diversos tipos de recursos.

O outro extremo seria tornar o monitoramento um caso de uso. Suponha que você só quer saber quantos processos estão aguardando por um Resource em um determinado momento:

>>> import simpy
>>>
>>> class MonitoredResource(simpy.Resource):
...     def __init__(self, *args, **kwargs):
...         super().__init__(*args, **kwargs)
...         self.data = []
...
...     def request(self, *args, **kwargs):
...         self.data.append((self._env.now, len(self.queue)))
...         return super().request(*args, **kwargs)
...
...     def release(self, *args, **kwargs):
...         self.data.append((self._env.now, len(self.queue)))
...         return super().release(*args, **kwargs)
>>>
>>> def test_process(env, res):
...     with res.request() as req:
...         yield req
...         yield env.timeout(1)
>>>
>>> env = simpy.Environment()
>>>
>>> res = MonitoredResource(env, capacity=1)
>>> p1 = env.process(test_process(env, res))
>>> p2 = env.process(test_process(env, res))
>>> env.run()
>>>
>>> print(res.data)
[(0, 0), (0, 0), (1, 1), (2, 0)]

Ao contrário do primeiro exemplo, agora não se tem um só recurso modificado/patched mas sim uma classe toda. Também foi removida toda a parte considerada adaptável do primeiro exemplo: Só é monitorado o Resource nomeado como tal, só é coletado o dado antes das requisições serem feitas e só é coletado/registrado o momento/tempo e o tamanho da fila. Porém, você precisaria de menos da metade do código.

Rastrear Eventos

Com o objetivo de fazer um debug ou visualizar uma simulação, você pode querer rastrear quando os eventos são criados, acionados e processados. Talvez você também queira rastrear quais processos criaram um determinado evento e que processos aguardaram um determinado evento.

As duas funções mais interessantes para esse caso de uso são Environment.step(), onde todos os eventos são processados e Environment.schedule(), onde todos os eventos são agendados e inseridos em uma fila de eventos do Simpy.

Aqui temos um exemplo que demonstra como Environment.step() pode ser adaptado de modo a rastrear todos os eventos processados:

>>> from functools import partial, wraps
>>> import simpy
>>>
>>> def trace(env, callback):
...     """Substitui o método ``step()`` de *env* por uma funcao de rastreio
...     que executa *callbacks* informando o momento do evento, prioridade,
...     identificacao e sua instancia antes de que ela seja processada.
...
...     """
...     def get_wrapper(env_step, callback):
...         """Gerando o wrapper para o metodo env.step()."""
...         @wraps(env_step)
...         def tracing_step():
...             """Executa *callback* para o proximo evento, caso tenha
...                ocorrido um antes executando ``env.step()``."""
...             if len(env._queue):
...                 t, prio, eid, event = env._queue[0]
...                 callback(t, prio, eid, event)
...             return env_step()
...         return tracing_step
...
...     env.step = get_wrapper(env.step, callback)
>>>
>>> def monitor(data, t, prio, eid, event):
...     data.append((t, eid, type(event)))
>>>
>>> def test_process(env):
...     yield env.timeout(1)
>>>
>>> data = []
>>> # Vincular *data* como o primeiro argumento de monitor()
>>> # mais em https://docs.python.org/3/library/functools.html#functools.partial
>>> monitor = partial(monitor, data)
>>>
>>> env = simpy.Environment()
>>> trace(env, monitor)
>>>
>>> p = env.process(test_process(env))
>>> env.run(until=p)
>>>
>>> for d in data:
...     print(d)
(0, 0, <class 'simpy.events.Initialize'>)
(1, 1, <class 'simpy.events.Timeout'>)
(1, 2, <class 'simpy.events.Process'>)

O exemplo acima é baseado em um pull request de Steve Pothier.

Usando os mesmos conceitos, você também pode fazer um patch do método Environment.schedule(). Isso lhe dará acesso ao centro da informação quando determinado evento é agendado e para que instante.

Além disso, você também pode modificar algumas ou todas as classes de evento do Simpy, por ex. o método __init__() a fim de rastrear quando e como um evento está sendo criado.