mirror of
https://github.com/reactos/reactos.git
synced 2025-08-05 00:45:43 +00:00
- Added comments to the fiber test, plus minor fixes
- Fixed NT_TIB structure to be compatible with w32api - Fixed fiber support in kernel32.dll. Now the correct argument is passed to fiber startup routines svn path=/trunk/; revision=8573
This commit is contained in:
parent
783cbe0718
commit
61ae1aa515
6 changed files with 78 additions and 31 deletions
|
@ -1,4 +1,4 @@
|
||||||
# $Id: Makefile,v 1.1 2004/03/07 03:15:20 hyperion Exp $
|
# $Id: Makefile,v 1.2 2004/03/07 20:07:04 hyperion Exp $
|
||||||
|
|
||||||
PATH_TO_TOP = ../../..
|
PATH_TO_TOP = ../../..
|
||||||
|
|
||||||
|
@ -10,8 +10,6 @@ TARGET_APPTYPE = console
|
||||||
|
|
||||||
TARGET_NAME = fiber
|
TARGET_NAME = fiber
|
||||||
|
|
||||||
TARGET_SDKLIBS = kernel32.a
|
|
||||||
|
|
||||||
TARGET_OBJECTS = $(TARGET_NAME).o
|
TARGET_OBJECTS = $(TARGET_NAME).o
|
||||||
|
|
||||||
TARGET_CFLAGS = -Wall -Werror -D__USE_W32API
|
TARGET_CFLAGS = -Wall -Werror -D__USE_W32API
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
/* $Id: fiber.c,v 1.1 2004/03/07 03:15:20 hyperion Exp $
|
/* $Id: fiber.c,v 1.2 2004/03/07 20:07:04 hyperion Exp $
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdlib.h>
|
#include <limits.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
#include <tchar.h>
|
#include <tchar.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
@ -62,14 +64,14 @@ struct FiberData
|
||||||
unsigned nRealPrio;
|
unsigned nRealPrio;
|
||||||
PVOID pFiber;
|
PVOID pFiber;
|
||||||
LIST_ENTRY leQueue;
|
LIST_ENTRY leQueue;
|
||||||
int nTickQueued;
|
int nQuantumQueued;
|
||||||
int nBoost;
|
int nBoost;
|
||||||
struct FiberData * pfdPrev;
|
struct FiberData * pfdPrev;
|
||||||
int bExitPrev;
|
int bExitPrev;
|
||||||
};
|
};
|
||||||
|
|
||||||
static LIST_ENTRY a_leQueues[32];
|
static LIST_ENTRY a_leQueues[32];
|
||||||
static unsigned nTick = 0;
|
static unsigned nQuantum = 0;
|
||||||
static struct FiberData * pfdLastStarveScan = NULL;
|
static struct FiberData * pfdLastStarveScan = NULL;
|
||||||
|
|
||||||
void Fbt_Create(int);
|
void Fbt_Create(int);
|
||||||
|
@ -100,7 +102,7 @@ void Fbt_Yield(VOID)
|
||||||
|
|
||||||
pfdCur = Fbt_GetCurrent();
|
pfdCur = Fbt_GetCurrent();
|
||||||
|
|
||||||
if(pfdCur->nBoost > 1)
|
if(pfdCur->nBoost)
|
||||||
{
|
{
|
||||||
-- pfdCur->nBoost;
|
-- pfdCur->nBoost;
|
||||||
|
|
||||||
|
@ -117,8 +119,10 @@ void Fbt_AfterSwitch(struct FiberData * pfdCur)
|
||||||
|
|
||||||
pfdPrev = pfdCur->pfdPrev;
|
pfdPrev = pfdCur->pfdPrev;
|
||||||
|
|
||||||
|
/* The previous fiber left some homework for us */
|
||||||
if(pfdPrev)
|
if(pfdPrev)
|
||||||
{
|
{
|
||||||
|
/* Kill the predecessor */
|
||||||
if(pfdCur->bExitPrev)
|
if(pfdCur->bExitPrev)
|
||||||
{
|
{
|
||||||
if(pfdLastStarveScan == pfdPrev)
|
if(pfdLastStarveScan == pfdPrev)
|
||||||
|
@ -127,16 +131,20 @@ void Fbt_AfterSwitch(struct FiberData * pfdCur)
|
||||||
DeleteFiber(pfdPrev->pFiber);
|
DeleteFiber(pfdPrev->pFiber);
|
||||||
free(pfdPrev);
|
free(pfdPrev);
|
||||||
}
|
}
|
||||||
|
/* Enqueue the previous fiber in the correct ready queue */
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
pfdPrev->nTickQueued = nTick;
|
/* Remember the quantum in which the previous fiber was queued */
|
||||||
|
pfdPrev->nQuantumQueued = nQuantum;
|
||||||
|
|
||||||
|
/* Disable the anti-starvation boost */
|
||||||
if(pfdPrev->nBoost)
|
if(pfdPrev->nBoost)
|
||||||
{
|
{
|
||||||
pfdPrev->nBoost = 0;
|
pfdPrev->nBoost = 0;
|
||||||
pfdPrev->nPrio = pfdPrev->nRealPrio;
|
pfdPrev->nPrio = pfdPrev->nRealPrio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Enqueue the previous fiber */
|
||||||
InsertTailList
|
InsertTailList
|
||||||
(
|
(
|
||||||
&a_leQueues[pfdPrev->nPrio],
|
&a_leQueues[pfdPrev->nPrio],
|
||||||
|
@ -162,9 +170,11 @@ void Fbt_Dispatch(struct FiberData * pfdCur, int bExit)
|
||||||
|
|
||||||
assert(pfdCur == GetFiberData());
|
assert(pfdCur == GetFiberData());
|
||||||
|
|
||||||
++ nTick;
|
++ nQuantum;
|
||||||
|
|
||||||
if(nTick % 10 == 0)
|
/* Every ten quantums check for starving threads */
|
||||||
|
/* FIXME: this implementation of starvation prevention isn't that great */
|
||||||
|
if(nQuantum % 10 == 0)
|
||||||
{
|
{
|
||||||
int j;
|
int j;
|
||||||
int k;
|
int k;
|
||||||
|
@ -175,34 +185,53 @@ void Fbt_Dispatch(struct FiberData * pfdCur, int bExit)
|
||||||
bResume = 0;
|
bResume = 0;
|
||||||
i = 0;
|
i = 0;
|
||||||
|
|
||||||
|
/* Pick up from where we left last time */
|
||||||
if(pfdLastStarveScan)
|
if(pfdLastStarveScan)
|
||||||
{
|
{
|
||||||
unsigned nPrio;
|
unsigned nPrio;
|
||||||
|
|
||||||
nPrio = pfdLastStarveScan->nPrio;
|
nPrio = pfdLastStarveScan->nPrio;
|
||||||
|
|
||||||
|
/* The last fiber we scanned for starvation isn't queued anymore */
|
||||||
if(IsListEmpty(&pfdLastStarveScan->leQueue))
|
if(IsListEmpty(&pfdLastStarveScan->leQueue))
|
||||||
|
/* Scan the ready queue for its priority */
|
||||||
i = nPrio;
|
i = nPrio;
|
||||||
|
/* Last fiber for its priority level */
|
||||||
else if(pfdLastStarveScan->leQueue.Flink == &a_leQueues[nPrio])
|
else if(pfdLastStarveScan->leQueue.Flink == &a_leQueues[nPrio])
|
||||||
|
/* Scan the ready queue for the next priority level */
|
||||||
i = nPrio + 1;
|
i = nPrio + 1;
|
||||||
|
/* Scan the next fiber in the ready queue */
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
i = nPrio;
|
||||||
ple = pfdLastStarveScan->leQueue.Flink;
|
ple = pfdLastStarveScan->leQueue.Flink;
|
||||||
bResume = 1;
|
bResume = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Priority levels 15-31 are never checked for starvation */
|
||||||
if(i >= 15)
|
if(i >= 15)
|
||||||
|
{
|
||||||
|
if(bResume)
|
||||||
|
bResume = 0;
|
||||||
|
|
||||||
i = 0;
|
i = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Scan at most 16 threads, in the priority range 0-14, applying in total at
|
||||||
|
most 10 boosts. This loop scales O(1)
|
||||||
|
*/
|
||||||
for(j = 0, k = 0, b = 0; j < 16 && k < 15 && b < 10; ++ j)
|
for(j = 0, k = 0, b = 0; j < 16 && k < 15 && b < 10; ++ j)
|
||||||
{
|
{
|
||||||
unsigned nDiff;
|
unsigned nDiff;
|
||||||
|
|
||||||
|
/* No previous state to resume from */
|
||||||
if(!bResume)
|
if(!bResume)
|
||||||
{
|
{
|
||||||
int nQueue;
|
int nQueue;
|
||||||
|
|
||||||
|
/* Get the first element in the current queue */
|
||||||
nQueue = (k + i) % 15;
|
nQueue = (k + i) % 15;
|
||||||
|
|
||||||
if(IsListEmpty(&a_leQueues[nQueue]))
|
if(IsListEmpty(&a_leQueues[nQueue]))
|
||||||
|
@ -216,23 +245,29 @@ void Fbt_Dispatch(struct FiberData * pfdCur, int bExit)
|
||||||
else
|
else
|
||||||
bResume = 0;
|
bResume = 0;
|
||||||
|
|
||||||
|
/* Get the current fiber */
|
||||||
pfdLastStarveScan = CONTAINING_RECORD(ple, struct FiberData, leQueue);
|
pfdLastStarveScan = CONTAINING_RECORD(ple, struct FiberData, leQueue);
|
||||||
assert(pfdLastStarveScan->nMagic == 0x12345678);
|
assert(pfdLastStarveScan->nMagic == 0x12345678);
|
||||||
assert(pfdLastStarveScan != pfdCur);
|
assert(pfdLastStarveScan != pfdCur);
|
||||||
|
|
||||||
if(nTick > pfdLastStarveScan->nTickQueued)
|
/* Calculate the number of quantums the fiber has been in the queue */
|
||||||
nDiff = nTick - pfdLastStarveScan->nTickQueued;
|
if(nQuantum > pfdLastStarveScan->nQuantumQueued)
|
||||||
|
nDiff = nQuantum - pfdLastStarveScan->nQuantumQueued;
|
||||||
else
|
else
|
||||||
nDiff = pfdLastStarveScan->nTickQueued - nTick;
|
nDiff = UINT_MAX - pfdLastStarveScan->nQuantumQueued + nQuantum;
|
||||||
|
|
||||||
|
/* The fiber has been ready for more than 30 quantums: it's starving */
|
||||||
if(nDiff > 30)
|
if(nDiff > 30)
|
||||||
{
|
{
|
||||||
|
/* Plus one boost applied */
|
||||||
++ b;
|
++ b;
|
||||||
|
|
||||||
pfdLastStarveScan->nBoost = 2;
|
/* Apply the boost */
|
||||||
|
pfdLastStarveScan->nBoost = 1;
|
||||||
pfdLastStarveScan->nRealPrio = pfdLastStarveScan->nPrio;
|
pfdLastStarveScan->nRealPrio = pfdLastStarveScan->nPrio;
|
||||||
pfdLastStarveScan->nPrio = 15;
|
pfdLastStarveScan->nPrio = 15;
|
||||||
|
|
||||||
|
/* Re-enqueue the fiber in the correct priority queue */
|
||||||
RemoveEntryList(&pfdLastStarveScan->leQueue);
|
RemoveEntryList(&pfdLastStarveScan->leQueue);
|
||||||
InsertTailList(&a_leQueues[15], &pfdLastStarveScan->leQueue);
|
InsertTailList(&a_leQueues[15], &pfdLastStarveScan->leQueue);
|
||||||
}
|
}
|
||||||
|
@ -241,40 +276,53 @@ void Fbt_Dispatch(struct FiberData * pfdCur, int bExit)
|
||||||
|
|
||||||
pfdNext = NULL;
|
pfdNext = NULL;
|
||||||
|
|
||||||
|
/* This fiber is going to die: scan all ready queues */
|
||||||
if(bExit)
|
if(bExit)
|
||||||
n = 1;
|
n = 1;
|
||||||
|
/*
|
||||||
|
Scan only ready queues for priorities greater than or equal to the priority of
|
||||||
|
the current thread (round-robin)
|
||||||
|
*/
|
||||||
else
|
else
|
||||||
n = pfdCur->nPrio + 1;
|
n = pfdCur->nPrio + 1;
|
||||||
|
|
||||||
|
/* This loop scales O(1) */
|
||||||
for(i = 32; i >= n; -- i)
|
for(i = 32; i >= n; -- i)
|
||||||
{
|
{
|
||||||
PLIST_ENTRY pleNext;
|
PLIST_ENTRY pleNext;
|
||||||
|
|
||||||
|
/* No fiber ready for this priority level */
|
||||||
if(IsListEmpty(&a_leQueues[i - 1]))
|
if(IsListEmpty(&a_leQueues[i - 1]))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* Get the next ready fiber */
|
||||||
pleNext = RemoveHeadList(&a_leQueues[i - 1]);
|
pleNext = RemoveHeadList(&a_leQueues[i - 1]);
|
||||||
InitializeListHead(pleNext);
|
InitializeListHead(pleNext);
|
||||||
|
|
||||||
pfdNext = CONTAINING_RECORD(pleNext, struct FiberData, leQueue);
|
pfdNext = CONTAINING_RECORD(pleNext, struct FiberData, leQueue);
|
||||||
assert(pfdNext->pFiber != GetCurrentFiber());
|
assert(pfdNext->pFiber != GetCurrentFiber());
|
||||||
assert(pfdNext->nMagic == 0x12345678);
|
assert(pfdNext->nMagic == 0x12345678);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Next fiber chosen */
|
||||||
if(pfdNext)
|
if(pfdNext)
|
||||||
{
|
{
|
||||||
|
/* Give some homework to the next fiber */
|
||||||
pfdNext->pfdPrev = pfdCur;
|
pfdNext->pfdPrev = pfdCur;
|
||||||
pfdNext->bExitPrev = bExit;
|
pfdNext->bExitPrev = bExit;
|
||||||
|
|
||||||
|
/* Switch to the next fiber */
|
||||||
SwitchToFiber(pfdNext->pFiber);
|
SwitchToFiber(pfdNext->pFiber);
|
||||||
|
|
||||||
|
/* Complete the switch back to this fiber */
|
||||||
Fbt_AfterSwitch(pfdCur);
|
Fbt_AfterSwitch(pfdCur);
|
||||||
}
|
}
|
||||||
|
/* No next fiber, and current fiber exiting */
|
||||||
else if(bExit)
|
else if(bExit)
|
||||||
{
|
{
|
||||||
PVOID pCurFiber;
|
PVOID pCurFiber;
|
||||||
|
|
||||||
|
/* Delete the current fiber. This kills the thread and stops the simulation */
|
||||||
if(pfdLastStarveScan == pfdCur)
|
if(pfdLastStarveScan == pfdCur)
|
||||||
pfdLastStarveScan = NULL;
|
pfdLastStarveScan = NULL;
|
||||||
|
|
||||||
|
@ -282,6 +330,7 @@ void Fbt_Dispatch(struct FiberData * pfdCur, int bExit)
|
||||||
free(pfdCur);
|
free(pfdCur);
|
||||||
DeleteFiber(pCurFiber);
|
DeleteFiber(pCurFiber);
|
||||||
}
|
}
|
||||||
|
/* No next fiber: continue running the current one */
|
||||||
}
|
}
|
||||||
|
|
||||||
void Fbt_Exit(VOID)
|
void Fbt_Exit(VOID)
|
||||||
|
@ -322,7 +371,7 @@ void Fbt_CreateFiber(int bInitial)
|
||||||
pData->nId = InterlockedIncrement(&s_nFiberIdSeed);
|
pData->nId = InterlockedIncrement(&s_nFiberIdSeed);
|
||||||
pData->nPrio = rand() % 32;
|
pData->nPrio = rand() % 32;
|
||||||
pData->pFiber = pFiber;
|
pData->pFiber = pFiber;
|
||||||
pData->nTickQueued = 0;
|
pData->nQuantumQueued = 0;
|
||||||
pData->nBoost = 0;
|
pData->nBoost = 0;
|
||||||
pData->nRealPrio = pData->nPrio;
|
pData->nRealPrio = pData->nPrio;
|
||||||
pData->pfdPrev = NULL;
|
pData->pfdPrev = NULL;
|
||||||
|
|
|
@ -11485,7 +11485,7 @@ void STDCALL DeleteFiber(LPVOID lpFiber);
|
||||||
|
|
||||||
void STDCALL SwitchToFiber(LPVOID lpFiber);
|
void STDCALL SwitchToFiber(LPVOID lpFiber);
|
||||||
|
|
||||||
#define GetFiberData() *(LPVOID *)(((PNT_TIB)NtCurrentTeb())->Fib.FiberData)
|
#define GetFiberData() *(LPVOID *)(((PNT_TIB)NtCurrentTeb())->FiberData)
|
||||||
|
|
||||||
WINBOOL STDCALL
|
WINBOOL STDCALL
|
||||||
RegisterServicesProcess(DWORD ServicesProcessId);
|
RegisterServicesProcess(DWORD ServicesProcessId);
|
||||||
|
|
|
@ -56,7 +56,7 @@ typedef struct _NT_TIB {
|
||||||
union {
|
union {
|
||||||
PVOID FiberData; /* 10h */
|
PVOID FiberData; /* 10h */
|
||||||
ULONG Version; /* 10h */
|
ULONG Version; /* 10h */
|
||||||
} Fib;
|
};
|
||||||
PVOID ArbitraryUserPointer; /* 14h */
|
PVOID ArbitraryUserPointer; /* 14h */
|
||||||
struct _NT_TIB *Self; /* 18h */
|
struct _NT_TIB *Self; /* 18h */
|
||||||
} NT_TIB, *PNT_TIB;
|
} NT_TIB, *PNT_TIB;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* $Id: fiber.c,v 1.9 2004/01/23 21:16:04 ekohl Exp $
|
/* $Id: fiber.c,v 1.10 2004/03/07 20:07:05 hyperion Exp $
|
||||||
*
|
*
|
||||||
* FILE: lib/kernel32/thread/fiber.c
|
* FILE: lib/kernel32/thread/fiber.c
|
||||||
*
|
*
|
||||||
|
@ -60,8 +60,8 @@ BOOL WINAPI ConvertFiberToThread(void)
|
||||||
pTeb->IsFiber = FALSE;
|
pTeb->IsFiber = FALSE;
|
||||||
|
|
||||||
/* free the fiber */
|
/* free the fiber */
|
||||||
if(pTeb->Tib.Fib.FiberData != NULL)
|
if(pTeb->Tib.FiberData != NULL)
|
||||||
RtlFreeHeap(pTeb->Peb->ProcessHeap, 0, pTeb->Tib.Fib.FiberData);
|
RtlFreeHeap(pTeb->Peb->ProcessHeap, 0, pTeb->Tib.FiberData);
|
||||||
|
|
||||||
/* success */
|
/* success */
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
@ -86,7 +86,7 @@ LPVOID WINAPI ConvertThreadToFiberEx(LPVOID lpParameter, DWORD dwFlags)
|
||||||
PFIBER pfCurFiber;
|
PFIBER pfCurFiber;
|
||||||
|
|
||||||
/* the current thread is already a fiber */
|
/* the current thread is already a fiber */
|
||||||
if(pTeb->IsFiber && pTeb->Tib.Fib.FiberData) return pTeb->Tib.Fib.FiberData;
|
if(pTeb->IsFiber && pTeb->Tib.FiberData) return pTeb->Tib.FiberData;
|
||||||
|
|
||||||
/* allocate the fiber */
|
/* allocate the fiber */
|
||||||
pfCurFiber = (PFIBER)RtlAllocateHeap(pTeb->Peb->ProcessHeap, 0, sizeof(FIBER));
|
pfCurFiber = (PFIBER)RtlAllocateHeap(pTeb->Peb->ProcessHeap, 0, sizeof(FIBER));
|
||||||
|
@ -108,7 +108,7 @@ LPVOID WINAPI ConvertThreadToFiberEx(LPVOID lpParameter, DWORD dwFlags)
|
||||||
pfCurFiber->DeallocationStack = pTeb->DeallocationStack;
|
pfCurFiber->DeallocationStack = pTeb->DeallocationStack;
|
||||||
|
|
||||||
/* associate the fiber to the current thread */
|
/* associate the fiber to the current thread */
|
||||||
pTeb->Tib.Fib.FiberData = pfCurFiber;
|
pTeb->Tib.FiberData = pfCurFiber;
|
||||||
pTeb->IsFiber = TRUE;
|
pTeb->IsFiber = TRUE;
|
||||||
|
|
||||||
/* success */
|
/* success */
|
||||||
|
@ -265,7 +265,7 @@ void WINAPI DeleteFiber(LPVOID lpFiber)
|
||||||
RtlFreeHeap(pTeb->Peb->ProcessHeap, 0, lpFiber);
|
RtlFreeHeap(pTeb->Peb->ProcessHeap, 0, lpFiber);
|
||||||
|
|
||||||
/* the fiber is deleting itself: let the system deallocate the stack */
|
/* the fiber is deleting itself: let the system deallocate the stack */
|
||||||
if(pTeb->Tib.Fib.FiberData == lpFiber) ExitThread(1);
|
if(pTeb->Tib.FiberData == lpFiber) ExitThread(1);
|
||||||
|
|
||||||
/* deallocate the stack */
|
/* deallocate the stack */
|
||||||
NtFreeVirtualMemory
|
NtFreeVirtualMemory
|
||||||
|
@ -288,7 +288,7 @@ __declspec(noreturn) extern void WINAPI ThreadStartup
|
||||||
__declspec(noreturn) void WINAPI FiberStartup(PVOID lpStartAddress)
|
__declspec(noreturn) void WINAPI FiberStartup(PVOID lpStartAddress)
|
||||||
{
|
{
|
||||||
/* FIXME? this should be pretty accurate */
|
/* FIXME? this should be pretty accurate */
|
||||||
ThreadStartup(lpStartAddress, NtCurrentTeb()->Tib.Fib.FiberData);
|
ThreadStartup(lpStartAddress, GetFiberData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EOF */
|
/* EOF */
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* $Id: fiber.S,v 1.3 2004/03/07 04:00:39 hyperion Exp $
|
/* $Id: fiber.S,v 1.4 2004/03/07 20:07:05 hyperion Exp $
|
||||||
*
|
*
|
||||||
* COPYRIGHT: See COPYING in the top level directory
|
* COPYRIGHT: See COPYING in the top level directory
|
||||||
* PROJECT: ReactOS system libraries
|
* PROJECT: ReactOS system libraries
|
||||||
|
@ -25,7 +25,7 @@ _SwitchToFiber@4:
|
||||||
movl %fs:0x18, %ecx /* Teb = NtCurrentTeb() */
|
movl %fs:0x18, %ecx /* Teb = NtCurrentTeb() */
|
||||||
|
|
||||||
/* get the current fiber */
|
/* get the current fiber */
|
||||||
movl 0x10(%ecx), %eax /* Fiber = Teb->Tib.Fib.FiberData */
|
movl 0x10(%ecx), %eax /* Fiber = Teb->Tib.FiberData */
|
||||||
|
|
||||||
/* store the volatile context of the current fiber */
|
/* store the volatile context of the current fiber */
|
||||||
movl 0x0(%ecx), %edx
|
movl 0x0(%ecx), %edx
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue