mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Cleanup
This commit is contained in:
parent
7d44956fe0
commit
781ff5f843
@ -1,136 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*
|
||||
* This is an example provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*
|
||||
* FBAnimationPerformanceTracker
|
||||
* -----------------------------------------------------------------------
|
||||
*
|
||||
* This class provides animation performance tracking functionality. It basically
|
||||
* measures the app's frame rate during an operation, and reports this information.
|
||||
*
|
||||
* 1) In Foo's designated initializer, construct a tracker object
|
||||
*
|
||||
* 2) Add calls to -start and -stop in appropriate places, e.g. for a ScrollView
|
||||
*
|
||||
* - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||||
* [_apTracker start];
|
||||
* }
|
||||
*
|
||||
* - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
|
||||
* {
|
||||
* if (!scrollView.dragging) {
|
||||
* [_apTracker stop];
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
||||
* if (!decelerate) {
|
||||
* [_apTracker stop];
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Notes
|
||||
* -----
|
||||
* [] The tracker operates by creating a CADisplayLink object to measure the frame rate of the display
|
||||
* during start/stop interval.
|
||||
*
|
||||
* [] Calls to -stop that were not preceded by a matching call to -start have no effect.
|
||||
*
|
||||
* [] 2 calls to -start in a row will trash the data accumulated so far and not log anything.
|
||||
*
|
||||
*
|
||||
* Configuration object for the core tracker
|
||||
*
|
||||
* ===============================================================================
|
||||
* I highly recommend for you to use the standard configuration provided
|
||||
* These are essentially here so that the computation of the metric is transparent
|
||||
* and you can feel confident in what the numbers mean.
|
||||
* ===============================================================================
|
||||
*/
|
||||
struct FBAnimationPerformanceTrackerConfig
|
||||
{
|
||||
// Number of frame drop that defines a "small" drop event. By default, 1.
|
||||
NSInteger smallDropEventFrameNumber;
|
||||
// Number of frame drop that defines a "large" drop event. By default, 4.
|
||||
NSInteger largeDropEventFrameNumber;
|
||||
// Number of maximum frame drops to which the drop will be trimmed down to. Currently 15.
|
||||
NSInteger maxFrameDropAccount;
|
||||
|
||||
// If YES, will report stack traces
|
||||
BOOL reportStackTraces;
|
||||
};
|
||||
typedef struct FBAnimationPerformanceTrackerConfig FBAnimationPerformanceTrackerConfig;
|
||||
|
||||
|
||||
@protocol FBAnimationPerformanceTrackerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Core Metric
|
||||
*
|
||||
* You are responsible for the aggregation of these metrics (it being on the client or the server). I recommend to implement both
|
||||
* to limit the payload you are sending to the server.
|
||||
*
|
||||
* The final recommended metric being: - SUM(duration) / SUM(smallDropEvent) aka the number of seconds between one frame drop or more
|
||||
* - SUM(duration) / SUM(largeDropEvent) aka the number of seconds between four frame drops or more
|
||||
*
|
||||
* The first metric will tell you how smooth is your scroll view.
|
||||
* The second metric will tell you how clowny your scroll view can get.
|
||||
*
|
||||
* Every time stop is called, this event will fire reporting the performance.
|
||||
*
|
||||
* NOTE on this metric:
|
||||
* - It has been tested at scale on many Facebook apps.
|
||||
* - It follows the curves of devices.
|
||||
* - You will need about 100K calls for the number to converge.
|
||||
* - It is perfectly correlated to X = Percentage of time spent at 60fps. Number of seconds between one frame drop = 1 / ( 1 - Time spent at 60 fps)
|
||||
* - We report fraction of drops. 7 frame drop = 1.75 of a large frame drop if a large drop is 4 frame drop.
|
||||
* This is to preserve the correlation mentionned above.
|
||||
*/
|
||||
- (void)reportDurationInMS:(NSInteger)duration smallDropEvent:(double)smallDropEvent largeDropEvent:(double)largeDropEvent;
|
||||
|
||||
/**
|
||||
* Stack traces
|
||||
*
|
||||
* Dark magic of the animation tracker. In case of a frame drop, this will return a stack trace.
|
||||
* This will NOT be reported on the main-thread, but off-main thread to save a few CPU cycles.
|
||||
*
|
||||
* The slide is constant value that needs to be reported with the stack for processing.
|
||||
* This currently only allows for symbolication of your own image.
|
||||
*
|
||||
* Future work includes symbolicating all modules. I personnaly find it usually
|
||||
* good enough to know the name of the module.
|
||||
*
|
||||
* The stack will have the following format:
|
||||
* Foundation:0x123|MyApp:0x234|MyApp:0x345|
|
||||
*
|
||||
* The slide will have the following format:
|
||||
* 0x456
|
||||
*/
|
||||
- (void)reportStackTrace:(NSString *)stack withSlide:(NSString *)slide;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBAnimationPerformanceTracker : NSObject
|
||||
|
||||
- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config;
|
||||
|
||||
+ (FBAnimationPerformanceTrackerConfig)standardConfig;
|
||||
|
||||
@property (weak, nonatomic, readwrite) id<FBAnimationPerformanceTrackerDelegate> delegate;
|
||||
|
||||
- (void)start;
|
||||
- (void)stop;
|
||||
|
||||
@end
|
@ -1,412 +0,0 @@
|
||||
//
|
||||
// FBAnimationPerformanceTracker.m
|
||||
// Display
|
||||
//
|
||||
// Created by Peter on 3/16/16.
|
||||
// Copyright © 2016 Telegram. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FBAnimationPerformanceTracker.h"
|
||||
|
||||
#import <dlfcn.h>
|
||||
#import <map>
|
||||
#import <pthread.h>
|
||||
|
||||
#import <QuartzCore/CADisplayLink.h>
|
||||
|
||||
#import <mach-o/dyld.h>
|
||||
|
||||
#import "execinfo.h"
|
||||
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
static BOOL _signalSetup;
|
||||
static pthread_t _mainThread;
|
||||
static NSThread *_trackerThread;
|
||||
|
||||
static std::map<void *, NSString *, std::greater<void *>> _imageNames;
|
||||
|
||||
#ifdef __LP64__
|
||||
typedef mach_header_64 fb_mach_header;
|
||||
typedef segment_command_64 fb_mach_segment_command;
|
||||
#define LC_SEGMENT_ARCH LC_SEGMENT_64
|
||||
#else
|
||||
typedef mach_header fb_mach_header;
|
||||
typedef segment_command fb_mach_segment_command;
|
||||
#define LC_SEGMENT_ARCH LC_SEGMENT
|
||||
#endif
|
||||
|
||||
static volatile BOOL _scrolling;
|
||||
pthread_mutex_t _scrollingMutex;
|
||||
pthread_cond_t _scrollingCondVariable;
|
||||
dispatch_queue_t _symbolicationQueue;
|
||||
|
||||
// We record at most 16 frames since I cap the number of frames dropped measured at 15.
|
||||
// Past 15, something went very wrong (massive contention, priority inversion, rpc call going wrong...) .
|
||||
// It will only pollute the data to get more.
|
||||
static const int callstack_max_number = 16;
|
||||
|
||||
static int callstack_i;
|
||||
static bool callstack_dirty;
|
||||
static int callstack_size[callstack_max_number];
|
||||
static void *callstacks[callstack_max_number][128];
|
||||
uint64_t callstack_time_capture;
|
||||
|
||||
static void _callstack_signal_handler(int signr, siginfo_t *info, void *secret)
|
||||
{
|
||||
// This is run on the main thread every 16 ms or so during scroll.
|
||||
|
||||
// Signals are run one by one so there is no risk of concurrency of a signal
|
||||
// by the same signal.
|
||||
|
||||
// The backtrace call is technically signal-safe on Unix-based system
|
||||
// See: http://www.unix.com/man-page/all/3c/walkcontext/
|
||||
|
||||
// WARNING: this is signal handler, no memory allocation is safe.
|
||||
// Essentially nothing is safe unless specified it is.
|
||||
callstack_size[callstack_i] = backtrace(callstacks[callstack_i], 128);
|
||||
callstack_i = (callstack_i + 1) & (callstack_max_number - 1); // & is a cheap modulo (only works for power of 2)
|
||||
callstack_dirty = true;
|
||||
}
|
||||
|
||||
@interface FBCallstack : NSObject
|
||||
@property (nonatomic, readonly, assign) int size;
|
||||
@property (nonatomic, readonly, assign) void **callstack;
|
||||
- (instancetype)initWithSize:(int)size callstack:(void *)callstack;
|
||||
@end
|
||||
|
||||
@implementation FBCallstack
|
||||
- (instancetype)initWithSize:(int)size callstack:(void *)callstack
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_size = size;
|
||||
_callstack = (void **)malloc(size * sizeof(void *));
|
||||
memcpy(_callstack, callstack, size * sizeof(void *));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
free(_callstack);
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation FBAnimationPerformanceTracker
|
||||
{
|
||||
FBAnimationPerformanceTrackerConfig _config;
|
||||
|
||||
BOOL _tracking;
|
||||
BOOL _firstUpdate;
|
||||
NSTimeInterval _previousFrameTimestamp;
|
||||
CADisplayLink *_displayLink;
|
||||
BOOL _prepared;
|
||||
|
||||
// numbers used to track the performance metrics
|
||||
double _durationTotal;
|
||||
double _maxFrameTime;
|
||||
double _smallDrops;
|
||||
double _largeDrops;
|
||||
}
|
||||
|
||||
- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config
|
||||
{
|
||||
if (self = [super init]) {
|
||||
// Stack trace logging is not working well in debug mode
|
||||
// We don't want the data anyway. So let's bail.
|
||||
#if defined(DEBUG)
|
||||
config.reportStackTraces = NO;
|
||||
#endif
|
||||
_config = config;
|
||||
if (config.reportStackTraces) {
|
||||
[self _setupSignal];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (FBAnimationPerformanceTrackerConfig)standardConfig
|
||||
{
|
||||
FBAnimationPerformanceTrackerConfig config = {
|
||||
.smallDropEventFrameNumber = 1,
|
||||
.largeDropEventFrameNumber = 4,
|
||||
.maxFrameDropAccount = 15,
|
||||
.reportStackTraces = NO
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
+ (void)_trackerLoop
|
||||
{
|
||||
while (true) {
|
||||
// If you are confused by this part,
|
||||
// Check out https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables
|
||||
|
||||
// Lock the mutex
|
||||
pthread_mutex_lock(&_scrollingMutex);
|
||||
while (!_scrolling) {
|
||||
// Unlock the mutex and sleep until the conditional variable is signaled
|
||||
pthread_cond_wait(&_scrollingCondVariable, &_scrollingMutex);
|
||||
// The conditional variable was signaled, but we need to check _scrolling
|
||||
// As nothing guarantees that it is still true
|
||||
}
|
||||
// _scrolling is true, go ahead and capture traces for a while.
|
||||
pthread_mutex_unlock(&_scrollingMutex);
|
||||
|
||||
// We are scrolling, yay, capture traces
|
||||
while (_scrolling) {
|
||||
usleep(16000);
|
||||
|
||||
// Here I use SIGPROF which is a signal supposed to be used for profiling
|
||||
// I haven't stumbled upon any collision so far.
|
||||
// There is no guarantee that it won't impact the system in unpredicted ways.
|
||||
// Use wisely.
|
||||
|
||||
pthread_kill(_mainThread, SIGPROF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_setupSignal
|
||||
{
|
||||
if (!_signalSetup) {
|
||||
// The signal hook should be setup once and only once
|
||||
_signalSetup = YES;
|
||||
|
||||
// I actually don't know if the main thread can die. If it does, well,
|
||||
// this is not going to work.
|
||||
// UPDATE 4/2015: on iOS8, it looks like the main-thread never dies, and this pointer is correct
|
||||
_mainThread = pthread_self();
|
||||
|
||||
callstack_i = 0;
|
||||
|
||||
// Setup the signal
|
||||
struct sigaction sa;
|
||||
sigfillset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sa.sa_sigaction = _callstack_signal_handler;
|
||||
sigaction(SIGPROF, &sa, NULL);
|
||||
|
||||
pthread_mutex_init(&_scrollingMutex, NULL);
|
||||
pthread_cond_init (&_scrollingCondVariable, NULL);
|
||||
|
||||
// Setup the signal firing loop
|
||||
_trackerThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(_trackerLoop) object:nil];
|
||||
// We wanna be higher priority than the main thread
|
||||
// On iOS8 : this will roughly stick us at priority 61, while the main thread oscillates between 20 and 47
|
||||
_trackerThread.threadPriority = 1.0;
|
||||
[_trackerThread start];
|
||||
|
||||
_symbolicationQueue = dispatch_queue_create("com.facebook.symbolication", DISPATCH_QUEUE_SERIAL);
|
||||
dispatch_async(_symbolicationQueue, ^(void) {[self _setupSymbolication];});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_setupSymbolication
|
||||
{
|
||||
// This extract the starting slide of every module in the app
|
||||
// This is used to know which module an instruction pointer belongs to.
|
||||
|
||||
// These operations is NOT thread-safe according to Apple docs
|
||||
// Do not call this multiple times
|
||||
int images = _dyld_image_count();
|
||||
|
||||
for (int i = 0; i < images; i ++) {
|
||||
intptr_t imageSlide = _dyld_get_image_vmaddr_slide(i);
|
||||
|
||||
// Here we extract the module name from the full path
|
||||
// Typically it looks something like: /path/to/lib/UIKit
|
||||
// And I just extract UIKit
|
||||
NSString *fullName = [NSString stringWithUTF8String:_dyld_get_image_name(i)];
|
||||
NSRange range = [fullName rangeOfString:@"/" options:NSBackwardsSearch];
|
||||
NSUInteger startP = (range.location != NSNotFound) ? range.location + 1 : 0;
|
||||
NSString *imageName = [fullName substringFromIndex:startP];
|
||||
|
||||
// This is parsing the mach header in order to extract the slide.
|
||||
// See https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/index.html
|
||||
// For the structure of mach headers
|
||||
fb_mach_header *header = (fb_mach_header*)_dyld_get_image_header(i);
|
||||
if (!header) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const struct load_command *cmd =
|
||||
reinterpret_cast<const struct load_command *>(header + 1);
|
||||
|
||||
for (unsigned int c = 0; cmd && (c < header->ncmds); c++) {
|
||||
if (cmd->cmd == LC_SEGMENT_ARCH) {
|
||||
const fb_mach_segment_command *seg =
|
||||
reinterpret_cast<const fb_mach_segment_command *>(cmd);
|
||||
|
||||
if (!strcmp(seg->segname, "__TEXT")) {
|
||||
_imageNames[(void *)(seg->vmaddr + imageSlide)] = imageName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cmd = reinterpret_cast<struct load_command*>((char *)cmd + cmd->cmdsize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_prepared) {
|
||||
[self _tearDownCADisplayLink];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Tracking
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (!_tracking) {
|
||||
if ([self prepare]) {
|
||||
_displayLink.paused = NO;
|
||||
_tracking = YES;
|
||||
[self _reset];
|
||||
|
||||
if (_config.reportStackTraces) {
|
||||
pthread_mutex_lock(&_scrollingMutex);
|
||||
_scrolling = YES;
|
||||
// Signal the tracker thread to start firing the signals
|
||||
pthread_cond_signal(&_scrollingCondVariable);
|
||||
pthread_mutex_unlock(&_scrollingMutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
if (_tracking) {
|
||||
_tracking = NO;
|
||||
_displayLink.paused = YES;
|
||||
if (_durationTotal > 0) {
|
||||
[_delegate reportDurationInMS:round(1000.0 * _durationTotal) smallDropEvent:_smallDrops largeDropEvent:_largeDrops];
|
||||
if (_config.reportStackTraces) {
|
||||
pthread_mutex_lock(&_scrollingMutex);
|
||||
_scrolling = NO;
|
||||
pthread_mutex_unlock(&_scrollingMutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)prepare
|
||||
{
|
||||
if (_prepared) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
[self _setUpCADisplayLink];
|
||||
_prepared = YES;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)_setUpCADisplayLink
|
||||
{
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update)];
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||
_displayLink.paused = YES;
|
||||
}
|
||||
|
||||
- (void)_tearDownCADisplayLink
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
}
|
||||
|
||||
- (void)_reset
|
||||
{
|
||||
_firstUpdate = YES;
|
||||
_previousFrameTimestamp = 0.0;
|
||||
_durationTotal = 0;
|
||||
_maxFrameTime = 0;
|
||||
_largeDrops = 0;
|
||||
_smallDrops = 0;
|
||||
}
|
||||
|
||||
- (void)_addFrameTime:(NSTimeInterval)actualFrameTime singleFrameTime:(NSTimeInterval)singleFrameTime
|
||||
{
|
||||
_maxFrameTime = MAX(actualFrameTime, _maxFrameTime);
|
||||
|
||||
NSInteger frameDropped = round(actualFrameTime / singleFrameTime) - 1;
|
||||
frameDropped = MAX(frameDropped, 0);
|
||||
// This is to reduce noise. Massive frame drops will just add noise to your data.
|
||||
frameDropped = MIN(_config.maxFrameDropAccount, frameDropped);
|
||||
|
||||
_durationTotal += (frameDropped + 1) * singleFrameTime;
|
||||
// We account 2 frame drops as 2 small events. This way the metric correlates perfectly with Time at X fps.
|
||||
_smallDrops += (frameDropped >= _config.smallDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.smallDropEventFrameNumber : 0.0;
|
||||
_largeDrops += (frameDropped >= _config.largeDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.largeDropEventFrameNumber : 0.0;
|
||||
|
||||
if (frameDropped >= 1) {
|
||||
if (_config.reportStackTraces) {
|
||||
callstack_dirty = false;
|
||||
for (int ci = 0; ci <= frameDropped ; ci ++) {
|
||||
// This is computing the previous indexes
|
||||
// callstack - 1 - ci takes us back ci frames
|
||||
// I want a positive number so I add callstack_max_number
|
||||
// And then just modulo it, with & (callstack_max_number - 1)
|
||||
int callstackPreviousIndex = ((callstack_i - 1 - ci) + callstack_max_number) & (callstack_max_number - 1);
|
||||
FBCallstack *callstackCopy = [[FBCallstack alloc] initWithSize:callstack_size[callstackPreviousIndex] callstack:callstacks[callstackPreviousIndex]];
|
||||
// Check that in between the beginning and the end of the copy the signal did not fire
|
||||
if (!callstack_dirty) {
|
||||
// The copy has been made. We are now fine, let's punt the rest off main-thread.
|
||||
__weak FBAnimationPerformanceTracker *weakSelf = self;
|
||||
dispatch_async(_symbolicationQueue, ^(void) {
|
||||
[weakSelf _reportStackTrace:callstackCopy];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_update
|
||||
{
|
||||
if (!_tracking) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_firstUpdate) {
|
||||
_firstUpdate = NO;
|
||||
_previousFrameTimestamp = _displayLink.timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval currentTimestamp = _displayLink.timestamp;
|
||||
NSTimeInterval frameTime = currentTimestamp - _previousFrameTimestamp;
|
||||
[self _addFrameTime:frameTime singleFrameTime:_displayLink.duration];
|
||||
_previousFrameTimestamp = currentTimestamp;
|
||||
}
|
||||
|
||||
- (void)_reportStackTrace:(FBCallstack *)callstack
|
||||
{
|
||||
static NSString *slide;
|
||||
static dispatch_once_t slide_predicate;
|
||||
|
||||
dispatch_once(&slide_predicate, ^{
|
||||
slide = [NSString stringWithFormat:@"%p", (void *)_dyld_get_image_header(0)];
|
||||
});
|
||||
|
||||
@autoreleasepool {
|
||||
NSMutableString *stack = [NSMutableString string];
|
||||
|
||||
for (int j = 2; j < callstack.size; j ++) {
|
||||
void *instructionPointer = callstack.callstack[j];
|
||||
auto it = _imageNames.lower_bound(instructionPointer);
|
||||
|
||||
NSString *imageName = (it != _imageNames.end()) ? it->second : @"???";
|
||||
|
||||
[stack appendString:imageName];
|
||||
[stack appendString:@":"];
|
||||
[stack appendString:[NSString stringWithFormat:@"%p", instructionPointer]];
|
||||
[stack appendString:@"|"];
|
||||
}
|
||||
|
||||
[_delegate reportStackTrace:stack withSlide:slide];
|
||||
}
|
||||
}
|
||||
@end
|
Loading…
x
Reference in New Issue
Block a user