Remove AtSubStart_Notify.
Allocate notify-related state lazily instead. This makes trivial subtransactions noticeably faster. Patch by me, reviewed and tested by Dilip Kumar, Kyotaro Horiguchi, and Jeevan Ladhe. Discussion: https://postgr.es/m/CA+TgmobE1J22S1eC-6N-je9LgrcwZypkwp+zH6JXo9mc=4Nk3A@mail.gmail.com
This commit is contained in:
parent
6837632b75
commit
967e276e9f
@ -4743,7 +4743,6 @@ StartSubTransaction(void)
|
|||||||
*/
|
*/
|
||||||
AtSubStart_Memory();
|
AtSubStart_Memory();
|
||||||
AtSubStart_ResourceOwner();
|
AtSubStart_ResourceOwner();
|
||||||
AtSubStart_Notify();
|
|
||||||
AfterTriggerBeginSubXact();
|
AfterTriggerBeginSubXact();
|
||||||
|
|
||||||
s->state = TRANS_INPROGRESS;
|
s->state = TRANS_INPROGRESS;
|
||||||
|
@ -344,9 +344,14 @@ typedef struct
|
|||||||
char channel[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
|
char channel[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
|
||||||
} ListenAction;
|
} ListenAction;
|
||||||
|
|
||||||
static List *pendingActions = NIL; /* list of ListenAction */
|
typedef struct ActionList
|
||||||
|
{
|
||||||
|
int nestingLevel; /* current transaction nesting depth */
|
||||||
|
List *actions; /* list of ListenAction structs */
|
||||||
|
struct ActionList *upper; /* details for upper transaction levels */
|
||||||
|
} ActionList;
|
||||||
|
|
||||||
static List *upperPendingActions = NIL; /* list of upper-xact lists */
|
static ActionList *pendingActions = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* State for outbound notifies consists of a list of all channels+payloads
|
* State for outbound notifies consists of a list of all channels+payloads
|
||||||
@ -385,8 +390,10 @@ typedef struct Notification
|
|||||||
|
|
||||||
typedef struct NotificationList
|
typedef struct NotificationList
|
||||||
{
|
{
|
||||||
|
int nestingLevel; /* current transaction nesting depth */
|
||||||
List *events; /* list of Notification structs */
|
List *events; /* list of Notification structs */
|
||||||
HTAB *hashtab; /* hash of NotificationHash structs, or NULL */
|
HTAB *hashtab; /* hash of NotificationHash structs, or NULL */
|
||||||
|
struct NotificationList *upper; /* details for upper transaction levels */
|
||||||
} NotificationList;
|
} NotificationList;
|
||||||
|
|
||||||
#define MIN_HASHABLE_NOTIFIES 16 /* threshold to build hashtab */
|
#define MIN_HASHABLE_NOTIFIES 16 /* threshold to build hashtab */
|
||||||
@ -396,9 +403,7 @@ typedef struct NotificationHash
|
|||||||
Notification *event; /* => the actual Notification struct */
|
Notification *event; /* => the actual Notification struct */
|
||||||
} NotificationHash;
|
} NotificationHash;
|
||||||
|
|
||||||
static NotificationList *pendingNotifies = NULL; /* current list, if any */
|
static NotificationList *pendingNotifies = NULL;
|
||||||
|
|
||||||
static List *upperPendingNotifies = NIL; /* list of upper-xact lists */
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Inbound notifications are initially processed by HandleNotifyInterrupt(),
|
* Inbound notifications are initially processed by HandleNotifyInterrupt(),
|
||||||
@ -609,6 +614,7 @@ pg_notify(PG_FUNCTION_ARGS)
|
|||||||
void
|
void
|
||||||
Async_Notify(const char *channel, const char *payload)
|
Async_Notify(const char *channel, const char *payload)
|
||||||
{
|
{
|
||||||
|
int my_level = GetCurrentTransactionNestLevel();
|
||||||
size_t channel_len;
|
size_t channel_len;
|
||||||
size_t payload_len;
|
size_t payload_len;
|
||||||
Notification *n;
|
Notification *n;
|
||||||
@ -659,25 +665,36 @@ Async_Notify(const char *channel, const char *payload)
|
|||||||
else
|
else
|
||||||
n->data[channel_len + 1] = '\0';
|
n->data[channel_len + 1] = '\0';
|
||||||
|
|
||||||
/* Now check for duplicates */
|
if (pendingNotifies == NULL || my_level > pendingNotifies->nestingLevel)
|
||||||
if (AsyncExistsPendingNotify(n))
|
|
||||||
{
|
{
|
||||||
/* It's a dup, so forget it */
|
NotificationList *notifies;
|
||||||
pfree(n);
|
|
||||||
MemoryContextSwitchTo(oldcontext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingNotifies == NULL)
|
/*
|
||||||
{
|
* First notify event in current (sub)xact. Note that we allocate the
|
||||||
/* First notify event in current (sub)xact */
|
* NotificationList in TopTransactionContext; the nestingLevel might
|
||||||
pendingNotifies = (NotificationList *) palloc(sizeof(NotificationList));
|
* get changed later by AtSubCommit_Notify.
|
||||||
pendingNotifies->events = list_make1(n);
|
*/
|
||||||
|
notifies = (NotificationList *)
|
||||||
|
MemoryContextAlloc(TopTransactionContext,
|
||||||
|
sizeof(NotificationList));
|
||||||
|
notifies->nestingLevel = my_level;
|
||||||
|
notifies->events = list_make1(n);
|
||||||
/* We certainly don't need a hashtable yet */
|
/* We certainly don't need a hashtable yet */
|
||||||
pendingNotifies->hashtab = NULL;
|
notifies->hashtab = NULL;
|
||||||
|
notifies->upper = pendingNotifies;
|
||||||
|
pendingNotifies = notifies;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
/* Now check for duplicates */
|
||||||
|
if (AsyncExistsPendingNotify(n))
|
||||||
|
{
|
||||||
|
/* It's a dup, so forget it */
|
||||||
|
pfree(n);
|
||||||
|
MemoryContextSwitchTo(oldcontext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Append more events to existing list */
|
/* Append more events to existing list */
|
||||||
AddEventToPendingNotifies(n);
|
AddEventToPendingNotifies(n);
|
||||||
}
|
}
|
||||||
@ -698,6 +715,7 @@ queue_listen(ListenActionKind action, const char *channel)
|
|||||||
{
|
{
|
||||||
MemoryContext oldcontext;
|
MemoryContext oldcontext;
|
||||||
ListenAction *actrec;
|
ListenAction *actrec;
|
||||||
|
int my_level = GetCurrentTransactionNestLevel();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Unlike Async_Notify, we don't try to collapse out duplicates. It would
|
* Unlike Async_Notify, we don't try to collapse out duplicates. It would
|
||||||
@ -713,7 +731,24 @@ queue_listen(ListenActionKind action, const char *channel)
|
|||||||
actrec->action = action;
|
actrec->action = action;
|
||||||
strcpy(actrec->channel, channel);
|
strcpy(actrec->channel, channel);
|
||||||
|
|
||||||
pendingActions = lappend(pendingActions, actrec);
|
if (pendingActions == NULL || my_level > pendingActions->nestingLevel)
|
||||||
|
{
|
||||||
|
ActionList *actions;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* First action in current sub(xact). Note that we allocate the
|
||||||
|
* ActionList in TopTransactionContext; the nestingLevel might get
|
||||||
|
* changed later by AtSubCommit_Notify.
|
||||||
|
*/
|
||||||
|
actions = (ActionList *)
|
||||||
|
MemoryContextAlloc(TopTransactionContext, sizeof(ActionList));
|
||||||
|
actions->nestingLevel = my_level;
|
||||||
|
actions->actions = list_make1(actrec);
|
||||||
|
actions->upper = pendingActions;
|
||||||
|
pendingActions = actions;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
pendingActions->actions = lappend(pendingActions->actions, actrec);
|
||||||
|
|
||||||
MemoryContextSwitchTo(oldcontext);
|
MemoryContextSwitchTo(oldcontext);
|
||||||
}
|
}
|
||||||
@ -744,7 +779,7 @@ Async_Unlisten(const char *channel)
|
|||||||
elog(DEBUG1, "Async_Unlisten(%s,%d)", channel, MyProcPid);
|
elog(DEBUG1, "Async_Unlisten(%s,%d)", channel, MyProcPid);
|
||||||
|
|
||||||
/* If we couldn't possibly be listening, no need to queue anything */
|
/* If we couldn't possibly be listening, no need to queue anything */
|
||||||
if (pendingActions == NIL && !unlistenExitRegistered)
|
if (pendingActions == NULL && !unlistenExitRegistered)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
queue_listen(LISTEN_UNLISTEN, channel);
|
queue_listen(LISTEN_UNLISTEN, channel);
|
||||||
@ -762,7 +797,7 @@ Async_UnlistenAll(void)
|
|||||||
elog(DEBUG1, "Async_UnlistenAll(%d)", MyProcPid);
|
elog(DEBUG1, "Async_UnlistenAll(%d)", MyProcPid);
|
||||||
|
|
||||||
/* If we couldn't possibly be listening, no need to queue anything */
|
/* If we couldn't possibly be listening, no need to queue anything */
|
||||||
if (pendingActions == NIL && !unlistenExitRegistered)
|
if (pendingActions == NULL && !unlistenExitRegistered)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
queue_listen(LISTEN_UNLISTEN_ALL, "");
|
queue_listen(LISTEN_UNLISTEN_ALL, "");
|
||||||
@ -858,21 +893,24 @@ PreCommit_Notify(void)
|
|||||||
elog(DEBUG1, "PreCommit_Notify");
|
elog(DEBUG1, "PreCommit_Notify");
|
||||||
|
|
||||||
/* Preflight for any pending listen/unlisten actions */
|
/* Preflight for any pending listen/unlisten actions */
|
||||||
foreach(p, pendingActions)
|
if (pendingActions != NULL)
|
||||||
{
|
{
|
||||||
ListenAction *actrec = (ListenAction *) lfirst(p);
|
foreach(p, pendingActions->actions)
|
||||||
|
|
||||||
switch (actrec->action)
|
|
||||||
{
|
{
|
||||||
case LISTEN_LISTEN:
|
ListenAction *actrec = (ListenAction *) lfirst(p);
|
||||||
Exec_ListenPreCommit();
|
|
||||||
break;
|
switch (actrec->action)
|
||||||
case LISTEN_UNLISTEN:
|
{
|
||||||
/* there is no Exec_UnlistenPreCommit() */
|
case LISTEN_LISTEN:
|
||||||
break;
|
Exec_ListenPreCommit();
|
||||||
case LISTEN_UNLISTEN_ALL:
|
break;
|
||||||
/* there is no Exec_UnlistenAllPreCommit() */
|
case LISTEN_UNLISTEN:
|
||||||
break;
|
/* there is no Exec_UnlistenPreCommit() */
|
||||||
|
break;
|
||||||
|
case LISTEN_UNLISTEN_ALL:
|
||||||
|
/* there is no Exec_UnlistenAllPreCommit() */
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -961,21 +999,24 @@ AtCommit_Notify(void)
|
|||||||
elog(DEBUG1, "AtCommit_Notify");
|
elog(DEBUG1, "AtCommit_Notify");
|
||||||
|
|
||||||
/* Perform any pending listen/unlisten actions */
|
/* Perform any pending listen/unlisten actions */
|
||||||
foreach(p, pendingActions)
|
if (pendingActions != NULL)
|
||||||
{
|
{
|
||||||
ListenAction *actrec = (ListenAction *) lfirst(p);
|
foreach(p, pendingActions->actions)
|
||||||
|
|
||||||
switch (actrec->action)
|
|
||||||
{
|
{
|
||||||
case LISTEN_LISTEN:
|
ListenAction *actrec = (ListenAction *) lfirst(p);
|
||||||
Exec_ListenCommit(actrec->channel);
|
|
||||||
break;
|
switch (actrec->action)
|
||||||
case LISTEN_UNLISTEN:
|
{
|
||||||
Exec_UnlistenCommit(actrec->channel);
|
case LISTEN_LISTEN:
|
||||||
break;
|
Exec_ListenCommit(actrec->channel);
|
||||||
case LISTEN_UNLISTEN_ALL:
|
break;
|
||||||
Exec_UnlistenAllCommit();
|
case LISTEN_UNLISTEN:
|
||||||
break;
|
Exec_UnlistenCommit(actrec->channel);
|
||||||
|
break;
|
||||||
|
case LISTEN_UNLISTEN_ALL:
|
||||||
|
Exec_UnlistenAllCommit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1705,36 +1746,6 @@ AtAbort_Notify(void)
|
|||||||
ClearPendingActionsAndNotifies();
|
ClearPendingActionsAndNotifies();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* AtSubStart_Notify() --- Take care of subtransaction start.
|
|
||||||
*
|
|
||||||
* Push empty state for the new subtransaction.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
AtSubStart_Notify(void)
|
|
||||||
{
|
|
||||||
MemoryContext old_cxt;
|
|
||||||
|
|
||||||
/* Keep the list-of-lists in TopTransactionContext for simplicity */
|
|
||||||
old_cxt = MemoryContextSwitchTo(TopTransactionContext);
|
|
||||||
|
|
||||||
upperPendingActions = lcons(pendingActions, upperPendingActions);
|
|
||||||
|
|
||||||
Assert(list_length(upperPendingActions) ==
|
|
||||||
GetCurrentTransactionNestLevel() - 1);
|
|
||||||
|
|
||||||
pendingActions = NIL;
|
|
||||||
|
|
||||||
upperPendingNotifies = lcons(pendingNotifies, upperPendingNotifies);
|
|
||||||
|
|
||||||
Assert(list_length(upperPendingNotifies) ==
|
|
||||||
GetCurrentTransactionNestLevel() - 1);
|
|
||||||
|
|
||||||
pendingNotifies = NULL;
|
|
||||||
|
|
||||||
MemoryContextSwitchTo(old_cxt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* AtSubCommit_Notify() --- Take care of subtransaction commit.
|
* AtSubCommit_Notify() --- Take care of subtransaction commit.
|
||||||
*
|
*
|
||||||
@ -1743,53 +1754,66 @@ AtSubStart_Notify(void)
|
|||||||
void
|
void
|
||||||
AtSubCommit_Notify(void)
|
AtSubCommit_Notify(void)
|
||||||
{
|
{
|
||||||
List *parentPendingActions;
|
int my_level = GetCurrentTransactionNestLevel();
|
||||||
NotificationList *parentPendingNotifies;
|
|
||||||
|
|
||||||
parentPendingActions = linitial_node(List, upperPendingActions);
|
/* If there are actions at our nesting level, we must reparent them. */
|
||||||
upperPendingActions = list_delete_first(upperPendingActions);
|
if (pendingActions != NULL &&
|
||||||
|
pendingActions->nestingLevel >= my_level)
|
||||||
Assert(list_length(upperPendingActions) ==
|
|
||||||
GetCurrentTransactionNestLevel() - 2);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Mustn't try to eliminate duplicates here --- see queue_listen()
|
|
||||||
*/
|
|
||||||
pendingActions = list_concat(parentPendingActions, pendingActions);
|
|
||||||
|
|
||||||
parentPendingNotifies = (NotificationList *) linitial(upperPendingNotifies);
|
|
||||||
upperPendingNotifies = list_delete_first(upperPendingNotifies);
|
|
||||||
|
|
||||||
Assert(list_length(upperPendingNotifies) ==
|
|
||||||
GetCurrentTransactionNestLevel() - 2);
|
|
||||||
|
|
||||||
if (pendingNotifies == NULL)
|
|
||||||
{
|
{
|
||||||
/* easy, no notify events happened in current subxact */
|
if (pendingActions->upper == NULL ||
|
||||||
pendingNotifies = parentPendingNotifies;
|
pendingActions->upper->nestingLevel < my_level - 1)
|
||||||
}
|
|
||||||
else if (parentPendingNotifies == NULL)
|
|
||||||
{
|
|
||||||
/* easy, subxact's list becomes parent's */
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Formerly, we didn't bother to eliminate duplicates here, but now we
|
|
||||||
* must, else we fall foul of "Assert(!found)", either here or during
|
|
||||||
* a later attempt to build the parent-level hashtable.
|
|
||||||
*/
|
|
||||||
NotificationList *childPendingNotifies = pendingNotifies;
|
|
||||||
ListCell *l;
|
|
||||||
|
|
||||||
pendingNotifies = parentPendingNotifies;
|
|
||||||
/* Insert all the subxact's events into parent, except for dups */
|
|
||||||
foreach(l, childPendingNotifies->events)
|
|
||||||
{
|
{
|
||||||
Notification *childn = (Notification *) lfirst(l);
|
/* nothing to merge; give the whole thing to the parent */
|
||||||
|
--pendingActions->nestingLevel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ActionList *childPendingActions = pendingActions;
|
||||||
|
|
||||||
if (!AsyncExistsPendingNotify(childn))
|
pendingActions = pendingActions->upper;
|
||||||
AddEventToPendingNotifies(childn);
|
|
||||||
|
/*
|
||||||
|
* Mustn't try to eliminate duplicates here --- see queue_listen()
|
||||||
|
*/
|
||||||
|
pendingActions->actions =
|
||||||
|
list_concat(pendingActions->actions,
|
||||||
|
childPendingActions->actions);
|
||||||
|
pfree(childPendingActions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If there are notifies at our nesting level, we must reparent them. */
|
||||||
|
if (pendingNotifies != NULL &&
|
||||||
|
pendingNotifies->nestingLevel >= my_level)
|
||||||
|
{
|
||||||
|
Assert(pendingNotifies->nestingLevel == my_level);
|
||||||
|
|
||||||
|
if (pendingNotifies->upper == NULL ||
|
||||||
|
pendingNotifies->upper->nestingLevel < my_level - 1)
|
||||||
|
{
|
||||||
|
/* nothing to merge; give the whole thing to the parent */
|
||||||
|
--pendingNotifies->nestingLevel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Formerly, we didn't bother to eliminate duplicates here, but
|
||||||
|
* now we must, else we fall foul of "Assert(!found)", either here
|
||||||
|
* or during a later attempt to build the parent-level hashtable.
|
||||||
|
*/
|
||||||
|
NotificationList *childPendingNotifies = pendingNotifies;
|
||||||
|
ListCell *l;
|
||||||
|
|
||||||
|
pendingNotifies = pendingNotifies->upper;
|
||||||
|
/* Insert all the subxact's events into parent, except for dups */
|
||||||
|
foreach(l, childPendingNotifies->events)
|
||||||
|
{
|
||||||
|
Notification *childn = (Notification *) lfirst(l);
|
||||||
|
|
||||||
|
if (!AsyncExistsPendingNotify(childn))
|
||||||
|
AddEventToPendingNotifies(childn);
|
||||||
|
}
|
||||||
|
pfree(childPendingNotifies);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1805,23 +1829,31 @@ AtSubAbort_Notify(void)
|
|||||||
/*
|
/*
|
||||||
* All we have to do is pop the stack --- the actions/notifies made in
|
* All we have to do is pop the stack --- the actions/notifies made in
|
||||||
* this subxact are no longer interesting, and the space will be freed
|
* this subxact are no longer interesting, and the space will be freed
|
||||||
* when CurTransactionContext is recycled.
|
* when CurTransactionContext is recycled. We still have to free the
|
||||||
|
* ActionList and NotificationList objects themselves, though, because
|
||||||
|
* those are allocated in TopTransactionContext.
|
||||||
*
|
*
|
||||||
* This routine could be called more than once at a given nesting level if
|
* Note that there might be no entries at all, or no entries for the
|
||||||
* there is trouble during subxact abort. Avoid dumping core by using
|
* current subtransaction level, either because none were ever created,
|
||||||
* GetCurrentTransactionNestLevel as the indicator of how far we need to
|
* or because we reentered this routine due to trouble during subxact
|
||||||
* prune the list.
|
* abort.
|
||||||
*/
|
*/
|
||||||
while (list_length(upperPendingActions) > my_level - 2)
|
while (pendingActions != NULL &&
|
||||||
|
pendingActions->nestingLevel >= my_level)
|
||||||
{
|
{
|
||||||
pendingActions = linitial_node(List, upperPendingActions);
|
ActionList *childPendingActions = pendingActions;
|
||||||
upperPendingActions = list_delete_first(upperPendingActions);
|
|
||||||
|
pendingActions = pendingActions->upper;
|
||||||
|
pfree(childPendingActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (list_length(upperPendingNotifies) > my_level - 2)
|
while (pendingNotifies != NULL &&
|
||||||
|
pendingNotifies->nestingLevel >= my_level)
|
||||||
{
|
{
|
||||||
pendingNotifies = (NotificationList *) linitial(upperPendingNotifies);
|
NotificationList *childPendingNotifies = pendingNotifies;
|
||||||
upperPendingNotifies = list_delete_first(upperPendingNotifies);
|
|
||||||
|
pendingNotifies = pendingNotifies->upper;
|
||||||
|
pfree(childPendingNotifies);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2374,12 +2406,11 @@ static void
|
|||||||
ClearPendingActionsAndNotifies(void)
|
ClearPendingActionsAndNotifies(void)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* We used to have to explicitly deallocate the list members and nodes,
|
* Everything's allocated in either TopTransactionContext or the context
|
||||||
* because they were malloc'd. Now, since we know they are palloc'd in
|
* for the subtransaction to which it corresponds. So, there's nothing
|
||||||
* CurTransactionContext, we need not do that --- they'll go away
|
* to do here except rest the pointers; the space will be reclaimed when
|
||||||
* automatically at transaction exit. We need only reset the list head
|
* the contexts are deleted.
|
||||||
* pointers.
|
|
||||||
*/
|
*/
|
||||||
pendingActions = NIL;
|
pendingActions = NULL;
|
||||||
pendingNotifies = NULL;
|
pendingNotifies = NULL;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,6 @@ extern void Async_UnlistenAll(void);
|
|||||||
extern void PreCommit_Notify(void);
|
extern void PreCommit_Notify(void);
|
||||||
extern void AtCommit_Notify(void);
|
extern void AtCommit_Notify(void);
|
||||||
extern void AtAbort_Notify(void);
|
extern void AtAbort_Notify(void);
|
||||||
extern void AtSubStart_Notify(void);
|
|
||||||
extern void AtSubCommit_Notify(void);
|
extern void AtSubCommit_Notify(void);
|
||||||
extern void AtSubAbort_Notify(void);
|
extern void AtSubAbort_Notify(void);
|
||||||
extern void AtPrepare_Notify(void);
|
extern void AtPrepare_Notify(void);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user