trace/plot update
This commit is contained in:
parent
d86609d8b3
commit
49fcc33a6b
21
plot.py
21
plot.py
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -11,15 +12,24 @@ def plottable(log: list[tuple[float, int]]):
|
|||||||
return np.array([[], []])
|
return np.array([[], []])
|
||||||
|
|
||||||
|
|
||||||
def plot():
|
def plot(fn: str):
|
||||||
plt.rcParams['figure.figsize'] = [16, 9]
|
plt.rcParams['figure.figsize'] = [16, 9]
|
||||||
plt.style.use("dark_background")
|
plt.style.use("dark_background")
|
||||||
plt.subplots_adjust(left=0.03, right=0.99, top=0.99, bottom=0.05)
|
plt.subplots_adjust(left=0.05, right=0.99, top=0.95, bottom=0.05)
|
||||||
|
plt.title(fn)
|
||||||
|
plt.xlabel('time (s)')
|
||||||
|
plt.ylabel('concurrency (1)')
|
||||||
|
|
||||||
with open('trace/latest.json') as file:
|
with open(fn) as file:
|
||||||
jsonified = json.load(file)
|
jsonified = json.load(file)
|
||||||
if (log := jsonified.get('DelayedResolver:sleep:concurrency')) is not None:
|
if (log := jsonified.get('DelayedResolver:sleep:concurrency')) is not None:
|
||||||
plt.plot(*plottable(log))
|
plt.plot(*plottable(log))
|
||||||
|
if (log := jsonified.get('ActiveBinaryTree:add:concurrency')) is not None:
|
||||||
|
plt.plot(*plottable(log))
|
||||||
|
if (log := jsonified.get('ActiveBinaryTree:contains:concurrency')) is not None:
|
||||||
|
plt.plot(*plottable(log))
|
||||||
|
if (log := jsonified.get('Stack:list:concurrency')) is not None:
|
||||||
|
plt.plot(*plottable(log))
|
||||||
if (log := jsonified.get('ActiveBinaryTree:add:entry')) is not None:
|
if (log := jsonified.get('ActiveBinaryTree:add:entry')) is not None:
|
||||||
plt.scatter(*plottable(log), c='tomato', zorder=100, s=.5)
|
plt.scatter(*plottable(log), c='tomato', zorder=100, s=.5)
|
||||||
if (log := jsonified.get('ActiveBinaryTree:add:exit')) is not None:
|
if (log := jsonified.get('ActiveBinaryTree:add:exit')) is not None:
|
||||||
@ -28,4 +38,7 @@ def plot():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
plot()
|
plot('trace/latest.json')
|
||||||
|
for fp in list(Path('trace').glob('*.json')):
|
||||||
|
if fp != Path('trace/latest.json'):
|
||||||
|
plot(str(fp))
|
||||||
|
@ -32,7 +32,7 @@ class Array(RecursiveMentionable, Generic[ElementType]):
|
|||||||
formatted = f'('
|
formatted = f'('
|
||||||
formatted += ''.join(
|
formatted += ''.join(
|
||||||
f'{tabulate(tab + 1)}{hash_point_str}'
|
f'{tabulate(tab + 1)}{hash_point_str}'
|
||||||
for hash_point_str in gather(
|
for hash_point_str in await gather(
|
||||||
*(
|
*(
|
||||||
hash_point_format(hash_point, tab + 1) for hash_point in self.array
|
hash_point_format(hash_point, tab + 1) for hash_point in self.array
|
||||||
)
|
)
|
||||||
|
4
rainbowadn/testing/instrument/__init__.py
Normal file
4
rainbowadn/testing/instrument/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .concurrency import Concurrency
|
||||||
|
from .counter import Counter
|
||||||
|
from .entryexit import EntryExit
|
||||||
|
from .instrumentation import Instrumentation
|
31
rainbowadn/testing/instrument/concurrency.py
Normal file
31
rainbowadn/testing/instrument/concurrency.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from .instrumentation import Instrumentation
|
||||||
|
|
||||||
|
__all__ = ('Concurrency',)
|
||||||
|
|
||||||
|
|
||||||
|
class Concurrency(Instrumentation):
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
def __init__(self, target, methodname: str):
|
||||||
|
assert isinstance(methodname, str)
|
||||||
|
super().__init__(target, methodname)
|
||||||
|
self.concurrency = 0
|
||||||
|
self.log: list[tuple[float, int]] = []
|
||||||
|
|
||||||
|
def time(self) -> float:
|
||||||
|
return time.time() - self.start
|
||||||
|
|
||||||
|
def point(self) -> tuple[float, int]:
|
||||||
|
return self.time(), self.concurrency
|
||||||
|
|
||||||
|
async def instrument(self, method, *args, **kwargs):
|
||||||
|
self.log.append(self.point())
|
||||||
|
self.concurrency += 1
|
||||||
|
self.log.append(self.point())
|
||||||
|
result = await method(*args, **kwargs)
|
||||||
|
self.log.append(self.point())
|
||||||
|
self.concurrency -= 1
|
||||||
|
self.log.append(self.point())
|
||||||
|
return result
|
14
rainbowadn/testing/instrument/counter.py
Normal file
14
rainbowadn/testing/instrument/counter.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from .instrumentation import Instrumentation
|
||||||
|
|
||||||
|
__all__ = ('Counter',)
|
||||||
|
|
||||||
|
|
||||||
|
class Counter(Instrumentation):
|
||||||
|
def __init__(self, target, methodname: str):
|
||||||
|
assert isinstance(methodname, str)
|
||||||
|
super().__init__(target, methodname)
|
||||||
|
self.counter = 0
|
||||||
|
|
||||||
|
def instrument(self, method, *args, **kwargs):
|
||||||
|
self.counter += 1
|
||||||
|
return method(*args, **kwargs)
|
21
rainbowadn/testing/instrument/entryexit.py
Normal file
21
rainbowadn/testing/instrument/entryexit.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from .instrumentation import Instrumentation
|
||||||
|
|
||||||
|
__all__ = ('EntryExit',)
|
||||||
|
|
||||||
|
|
||||||
|
class EntryExit(Instrumentation):
|
||||||
|
def __init__(self, target, methodname: str, point: Callable[[], tuple[float, int]]):
|
||||||
|
assert callable(point)
|
||||||
|
assert isinstance(methodname, str)
|
||||||
|
super().__init__(target, methodname)
|
||||||
|
self.point = point
|
||||||
|
self.entry_log: list[tuple[float, int]] = []
|
||||||
|
self.exit_log: list[tuple[float, int]] = []
|
||||||
|
|
||||||
|
async def instrument(self, method, *args, **kwargs):
|
||||||
|
self.entry_log.append(self.point())
|
||||||
|
result = await method(*args, **kwargs)
|
||||||
|
self.exit_log.append(self.point())
|
||||||
|
return result
|
53
rainbowadn/testing/instrument/instrumentation.py
Normal file
53
rainbowadn/testing/instrument/instrumentation.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import functools
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from typing import Callable, Generic, TypeVar
|
||||||
|
|
||||||
|
__all__ = ('Instrumentation',)
|
||||||
|
|
||||||
|
IType = TypeVar('IType')
|
||||||
|
|
||||||
|
|
||||||
|
class Instrumentation(Generic[IType]):
|
||||||
|
deinstrumentation = {}
|
||||||
|
method: Callable
|
||||||
|
wrap: Callable
|
||||||
|
|
||||||
|
def __init__(self, target, methodname: str):
|
||||||
|
assert isinstance(methodname, str)
|
||||||
|
assert callable(getattr(target, methodname))
|
||||||
|
self.target = target
|
||||||
|
self.methodname = methodname
|
||||||
|
|
||||||
|
def instrument(self, method, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __enter__(self: IType) -> IType:
|
||||||
|
assert not hasattr(self, 'method')
|
||||||
|
self.method = getattr(self.target, self.methodname)
|
||||||
|
assert callable(self.method)
|
||||||
|
|
||||||
|
@functools.wraps(self.method)
|
||||||
|
def wrap(*args, **kwargs):
|
||||||
|
return self.instrument(self.method, *args, **kwargs)
|
||||||
|
|
||||||
|
self.wrap = wrap
|
||||||
|
|
||||||
|
setattr(self.target, self.methodname, self.wrap)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def schedule_deinstrumentation(self):
|
||||||
|
self.deinstrumentation[self.wrap] = self.method
|
||||||
|
del self.wrap
|
||||||
|
del self.method
|
||||||
|
|
||||||
|
def deinstrument(self):
|
||||||
|
while (method := getattr(self.target, self.methodname)) in self.deinstrumentation:
|
||||||
|
setattr(self.target, self.methodname, self.deinstrumentation.pop(method))
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.schedule_deinstrumentation()
|
||||||
|
self.deinstrument()
|
||||||
|
|
||||||
|
def enter(self: IType, stack: ExitStack) -> IType:
|
||||||
|
return stack.enter_context(self)
|
@ -1,107 +0,0 @@
|
|||||||
import functools
|
|
||||||
import time
|
|
||||||
from contextlib import ExitStack
|
|
||||||
from typing import Callable, Generic, TypeVar
|
|
||||||
|
|
||||||
__all__ = ('Instrumentation', 'Counter', 'Concurrency', 'EntryExit',)
|
|
||||||
|
|
||||||
IType = TypeVar('IType')
|
|
||||||
|
|
||||||
|
|
||||||
class Instrumentation(Generic[IType]):
|
|
||||||
deinstrumentation = {}
|
|
||||||
method: Callable
|
|
||||||
wrap: Callable
|
|
||||||
|
|
||||||
def __init__(self, target, methodname: str):
|
|
||||||
assert isinstance(methodname, str)
|
|
||||||
assert callable(getattr(target, methodname))
|
|
||||||
self.target = target
|
|
||||||
self.methodname = methodname
|
|
||||||
|
|
||||||
def instrument(self, method, *args, **kwargs):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __enter__(self: IType) -> IType:
|
|
||||||
assert not hasattr(self, 'method')
|
|
||||||
self.method = getattr(self.target, self.methodname)
|
|
||||||
assert callable(self.method)
|
|
||||||
|
|
||||||
@functools.wraps(self.method)
|
|
||||||
def wrap(*args, **kwargs):
|
|
||||||
return self.instrument(self.method, *args, **kwargs)
|
|
||||||
|
|
||||||
self.wrap = wrap
|
|
||||||
|
|
||||||
setattr(self.target, self.methodname, self.wrap)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def schedule_deinstrumentation(self):
|
|
||||||
self.deinstrumentation[self.wrap] = self.method
|
|
||||||
del self.wrap
|
|
||||||
del self.method
|
|
||||||
|
|
||||||
def deinstrument(self):
|
|
||||||
while (method := getattr(self.target, self.methodname)) in self.deinstrumentation:
|
|
||||||
setattr(self.target, self.methodname, self.deinstrumentation.pop(method))
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.schedule_deinstrumentation()
|
|
||||||
self.deinstrument()
|
|
||||||
|
|
||||||
def enter(self: IType, stack: ExitStack) -> IType:
|
|
||||||
return stack.enter_context(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Counter(Instrumentation):
|
|
||||||
def __init__(self, target, methodname: str):
|
|
||||||
assert isinstance(methodname, str)
|
|
||||||
super().__init__(target, methodname)
|
|
||||||
self.counter = 0
|
|
||||||
|
|
||||||
def instrument(self, method, *args, **kwargs):
|
|
||||||
self.counter += 1
|
|
||||||
return method(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Concurrency(Instrumentation):
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
def __init__(self, target, methodname: str):
|
|
||||||
assert isinstance(methodname, str)
|
|
||||||
super().__init__(target, methodname)
|
|
||||||
self.concurrency = 0
|
|
||||||
self.log: list[tuple[float, int]] = []
|
|
||||||
|
|
||||||
def time(self) -> float:
|
|
||||||
return time.time() - self.start
|
|
||||||
|
|
||||||
def point(self) -> tuple[float, int]:
|
|
||||||
return self.time(), self.concurrency
|
|
||||||
|
|
||||||
async def instrument(self, method, *args, **kwargs):
|
|
||||||
self.log.append(self.point())
|
|
||||||
self.concurrency += 1
|
|
||||||
self.log.append(self.point())
|
|
||||||
result = await method(*args, **kwargs)
|
|
||||||
self.log.append(self.point())
|
|
||||||
self.concurrency -= 1
|
|
||||||
self.log.append(self.point())
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class EntryExit(Instrumentation):
|
|
||||||
def __init__(self, target, methodname: str, point: Callable[[], tuple[float, int]]):
|
|
||||||
assert callable(point)
|
|
||||||
assert isinstance(methodname, str)
|
|
||||||
super().__init__(target, methodname)
|
|
||||||
self.point = point
|
|
||||||
self.entry_log: list[tuple[float, int]] = []
|
|
||||||
self.exit_log: list[tuple[float, int]] = []
|
|
||||||
|
|
||||||
async def instrument(self, method, *args, **kwargs):
|
|
||||||
self.entry_log.append(self.point())
|
|
||||||
result = await method(*args, **kwargs)
|
|
||||||
self.exit_log.append(self.point())
|
|
||||||
return result
|
|
@ -16,7 +16,7 @@ from rainbowadn.encryption import *
|
|||||||
from rainbowadn.nullability import *
|
from rainbowadn.nullability import *
|
||||||
from rainbowadn.v13 import *
|
from rainbowadn.v13 import *
|
||||||
from rainbowadn.wrisbt import *
|
from rainbowadn.wrisbt import *
|
||||||
from .instrumentation import *
|
from .instrument import *
|
||||||
from .resolvers import *
|
from .resolvers import *
|
||||||
|
|
||||||
|
|
||||||
@ -83,10 +83,10 @@ class TestAll(unittest.IsolatedAsyncioTestCase):
|
|||||||
stoptime = now
|
stoptime = now
|
||||||
return delta
|
return delta
|
||||||
|
|
||||||
n = 2500
|
n = 5000
|
||||||
keysize = 7
|
keysize = 7
|
||||||
with self.subTest('create empty'):
|
with self.subTest('create empty'):
|
||||||
btree: WrisbtRoot = WrisbtRoot.empty(WrisbtParametres(1, keysize))
|
btree: WrisbtRoot = WrisbtRoot.empty(WrisbtParametres(5, keysize))
|
||||||
measure('init')
|
measure('init')
|
||||||
with self.subTest('add keys', n=n):
|
with self.subTest('add keys', n=n):
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
@ -158,7 +158,7 @@ class TestAll(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_encryption(self):
|
async def test_encryption(self):
|
||||||
set_gather_linear()
|
set_gather_linear()
|
||||||
instrumentation = Counter(Encrypted, 'encrypt')
|
encrypt_ctr = Counter(Encrypted, 'encrypt')
|
||||||
with self.subTest('setup'):
|
with self.subTest('setup'):
|
||||||
key = b'a' * 32
|
key = b'a' * 32
|
||||||
with self.subTest('create empty'):
|
with self.subTest('create empty'):
|
||||||
@ -171,9 +171,9 @@ class TestAll(unittest.IsolatedAsyncioTestCase):
|
|||||||
print(await tree.reference.str(0))
|
print(await tree.reference.str(0))
|
||||||
with self.subTest('encrypt'):
|
with self.subTest('encrypt'):
|
||||||
target = tree.reference
|
target = tree.reference
|
||||||
with instrumentation:
|
with encrypt_ctr:
|
||||||
target = (await Encrypted.encrypt(target, key)).decrypted
|
target = (await Encrypted.encrypt(target, key)).decrypted
|
||||||
print(instrumentation.counter)
|
print(encrypt_ctr.counter)
|
||||||
tree = tree.create(target)
|
tree = tree.create(target)
|
||||||
print(await tree.reference.str(0))
|
print(await tree.reference.str(0))
|
||||||
with self.subTest('alter'):
|
with self.subTest('alter'):
|
||||||
@ -182,13 +182,13 @@ class TestAll(unittest.IsolatedAsyncioTestCase):
|
|||||||
print(await tree.reference.str(0))
|
print(await tree.reference.str(0))
|
||||||
with self.subTest('encrypt and migrate'):
|
with self.subTest('encrypt and migrate'):
|
||||||
target = tree.reference
|
target = tree.reference
|
||||||
with instrumentation:
|
with encrypt_ctr:
|
||||||
eeed = await Encrypted.encrypt(target, key)
|
eeed = await Encrypted.encrypt(target, key)
|
||||||
print(instrumentation.counter)
|
print(encrypt_ctr.counter)
|
||||||
print(await (await self.dr().migrate_resolved(eeed)).decrypted.str(0))
|
print(await (await self.dr().migrate_resolved(eeed)).decrypted.str(0))
|
||||||
with self.subTest('re-encrypt'):
|
with self.subTest('re-encrypt'):
|
||||||
new_key = b'b' * 32
|
new_key = b'b' * 32
|
||||||
target = eeed.decrypted
|
target = eeed.decrypted
|
||||||
with instrumentation:
|
with encrypt_ctr:
|
||||||
await Encrypted.encrypt(target, new_key)
|
await Encrypted.encrypt(target, new_key)
|
||||||
print(instrumentation.counter)
|
print(encrypt_ctr.counter)
|
||||||
|
53
trace.py
53
trace.py
@ -10,10 +10,11 @@ from nacl.signing import SigningKey
|
|||||||
|
|
||||||
from plot import plot
|
from plot import plot
|
||||||
from rainbowadn.chain import *
|
from rainbowadn.chain import *
|
||||||
|
from rainbowadn.collection.linear import *
|
||||||
from rainbowadn.collection.trees.binary import *
|
from rainbowadn.collection.trees.binary import *
|
||||||
from rainbowadn.core import *
|
from rainbowadn.core import *
|
||||||
from rainbowadn.nullability import *
|
from rainbowadn.nullability import *
|
||||||
from rainbowadn.testing.instrumentation import *
|
from rainbowadn.testing.instrument import *
|
||||||
from rainbowadn.testing.resolvers import *
|
from rainbowadn.testing.resolvers import *
|
||||||
from rainbowadn.v13 import *
|
from rainbowadn.v13 import *
|
||||||
|
|
||||||
@ -33,11 +34,11 @@ def target_str(target) -> str:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def jsonify(instrumentation: Instrumentation) -> dict:
|
def jsonify(dumped: Instrumentation) -> dict:
|
||||||
prefix = f'{target_str(instrumentation.target)}:{instrumentation.methodname}'
|
prefix = f'{target_str(dumped.target)}:{dumped.methodname}'
|
||||||
match instrumentation:
|
match dumped:
|
||||||
case Counter(counter=counter):
|
case Counter(counter=ctr):
|
||||||
return {f'{prefix}:counter': counter}
|
return {f'{prefix}:counter': ctr}
|
||||||
case Concurrency(log=log):
|
case Concurrency(log=log):
|
||||||
return {f'{prefix}:concurrency': log}
|
return {f'{prefix}:concurrency': log}
|
||||||
case EntryExit(entry_log=entry_log, exit_log=exit_log):
|
case EntryExit(entry_log=entry_log, exit_log=exit_log):
|
||||||
@ -49,9 +50,7 @@ def jsonify(instrumentation: Instrumentation) -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def mock(bank: BankChain) -> BankChain:
|
||||||
set_gather_linear()
|
|
||||||
bank: BankChain = BankChain.empty(ReductionChainMetaFactory().loose())
|
|
||||||
key_0 = SigningKey.generate()
|
key_0 = SigningKey.generate()
|
||||||
transaction_0 = Transaction.make(
|
transaction_0 = Transaction.make(
|
||||||
[],
|
[],
|
||||||
@ -69,6 +68,24 @@ async def main():
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
return bank
|
||||||
|
|
||||||
|
|
||||||
|
def get_instrumentations() -> list[Instrumentation]:
|
||||||
|
sleep_cc = Concurrency(DelayedResolver, 'sleep')
|
||||||
|
return [
|
||||||
|
sleep_cc,
|
||||||
|
EntryExit(ActiveBinaryTree, 'add', sleep_cc.point),
|
||||||
|
Concurrency(ActiveBinaryTree, 'add'),
|
||||||
|
Concurrency(ActiveBinaryTree, 'contains'),
|
||||||
|
Concurrency(Stack, 'list'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
set_gather_linear()
|
||||||
|
bank: BankChain = BankChain.empty(ReductionChainMetaFactory().loose())
|
||||||
|
# bank = await mock(bank)
|
||||||
for _ in range(16):
|
for _ in range(16):
|
||||||
bank = await bank.adds(
|
bank = await bank.adds(
|
||||||
[
|
[
|
||||||
@ -87,19 +104,27 @@ async def main():
|
|||||||
)
|
)
|
||||||
print('saved')
|
print('saved')
|
||||||
set_gather_asyncio()
|
set_gather_asyncio()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as estack:
|
||||||
sleep_cc = Concurrency(DelayedResolver, 'sleep').enter(stack)
|
instrumentations = get_instrumentations()
|
||||||
avl_ex = EntryExit(ActiveBinaryTree, 'add', sleep_cc.point).enter(stack)
|
for stacked in instrumentations:
|
||||||
|
stacked.enter(estack)
|
||||||
assert_true(await bank.verify())
|
assert_true(await bank.verify())
|
||||||
print(Instrumentation.deinstrumentation)
|
print(Instrumentation.deinstrumentation)
|
||||||
|
print('traced')
|
||||||
fn = f'trace/{int(time.time())}-{os.urandom(2).hex()}.json'
|
fn = f'trace/{int(time.time())}-{os.urandom(2).hex()}.json'
|
||||||
|
jsonified = {}
|
||||||
|
for dumped in instrumentations:
|
||||||
|
jsonified |= jsonify(dumped)
|
||||||
with open(fn, 'w') as file:
|
with open(fn, 'w') as file:
|
||||||
json.dump(
|
json.dump(
|
||||||
jsonify(sleep_cc) | jsonify(avl_ex),
|
jsonified,
|
||||||
file
|
file
|
||||||
)
|
)
|
||||||
|
print('dumped')
|
||||||
shutil.copy(fn, f'trace/latest.json')
|
shutil.copy(fn, f'trace/latest.json')
|
||||||
plot()
|
print('copied')
|
||||||
|
plot(fn)
|
||||||
|
print('plotted')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
Loading…
Reference in New Issue
Block a user