Skip to content

Project3

Yeonguk Go edited this page Oct 8, 2021 · 1 revision

Project3 Design

Index

  1. Project3의 요구사항
  2. Project2에서 변경된 사항
  3. Layer 구조와 함수 상세정보
  4. Todo

1. Project3의 요구사항

on-disk pages의 cashing을 위한 in-memory buffer manager를 구현

  1. 명시된 형태에 own design을 추가한 buffer block structure 정의
  2. 명시된 동작을 수행하는 init_db, open_table, db_insert, db_find, db_delete, close_table, shutdown_db 함수를 API services로 제공
  3. 최대 10개의 table을 동시에 열 수 있도록 구현

2. Project2에서 변경된 사항

1. Layer 구조의 변경

  • Project2에서는 index manager와 disk space manager의 2개 layer로 구현했으며, 과제에서 요구한 API services는 index manager에 존재하도록 구현했으나, Project3에서는 buffer manager가 추가되며 추후 과제에서 더욱 추가될 수 있으므로, 제공해야 하는 API services를 위한 wrapper functions를 가지는 API layer를 추가하였다. (다른 학생들과 논의 중 전윤성 학생이 제안한 design)
  • 따라서, (상위) API layer - index manager - buffer manager - disk space manager (하위) 의 layer 구조를 가진다.
  • wrapper functions 순서도 Wrapper_Functions_for_API_Services

2. buffer manager 추가

  • buffer를 관리하기 위한 buffer manager layer (Project3의 요구사항)
  • 과제에서 구현을 요구한 buffer_block 구조체는, 명시된 것 외에, 해당 table에서의 탐색을 위한 buffer_block_t 포인터 next_of_table과 prev_of_table을 추가하였다.
  • buffer_table 구조체를 만들어서, table id와 file descripter 값, buffer에 올려진 해당 table에 해당하는 block의 개수, 이의 탐색을 위한 buffer_block_t 포인터 head_of_table을 사용하였다.
  • project3에서 추가적으로 요구한 init_db, shutdown_db, open_table, close_table의 실질적인 구현이 여기서 이루어졌으며, 이는 상위 layer의 wrapper function으로 연결된다.
  • index manager가 file manager에 존재하던 file_alloc_page와 file_free_page를 사용하던 것과 유사하게 사용할 수 있도록, buffer_alloc_page, buffer_free_page를 구현했으며,
  • file_read/write_page 대신 사용하도록 buffer_request_page / buffer_release_page를 추가하였다. 특히 다른 점은, 수정 사항이 없을 시 write를 하지 않아도 됐던 file manager와 다르게, 사용 완료 시 무조건 release를 해줘야 하며, paremeter로 받는 is_writed로 dirty 여부를 판단한다.
  • 자세한 내용은 buffer manager 상세정보 참고

3. index manager 수정

  • Project2에서는 index manager가 disk space manager 소속 함수를 호출하여 사용하였으나, Project3에서는 index manager의 각 함수가 disk space manager가 아닌 buffer manager 소속 함수를 호출하여 작업을 수행하도록 수정하였다.
  • 따라서, Project2에서는 file_read한 후, 필요하면 file_write하고, 필요하지 않으면 추가적인 조치를 취하지 않았으나, Project3에서는 buffer_request하며 pin을 꽂은 후, 사용이 완료되면 write 여부와 무관하게 buffer_release하며 pin을 해제한다. 또한 이 때 write 여부를 송신하여 is_dirty 값을 수정하도록 한다.
  • Project2에서는 단일 table을 사용하며 table 관련 정보를 index manager에서 따로 다루지 않았으나, Project3에서는 최대 10개 table을 동시에 열어서 사용할 수 있어야 하므로 각 함수가 table_id를 인자로 받고 그에 적합한 동작을 수행한다.
  • Project2에서는 이어지는 함수에 page number를 전달해서 해당 함수에서 다시 read하여 사용했으나, Project3에서는 이어지는 함수에 page buffer 포인터를 전달해서 해당 함수에서 buffer 상의 page에 바로 접근할 수 있도록 한다. 단, 한 cycle 동안 request된 page는 모두 그 안에서 release될 수 있도록 한다.
  • API layer의 wrapper functions가 호출할 함수 7개가 존재하며, 이 중 4개(index_init_db, index_shutdown_db, index_open_table, index_close_table)는 다시 buffer manager 함수의 wrapper functions이고, 3개(index_insert, index_find, index_delete)는 실제로 이 계층에서 작업을 수행한다.

4. disk space manager 수정

  • Project2에서는 단일 table을 사용하며 전역변수 fd로 table file을 관리했지만, Project3에서는 최대 10개의 table을 동시에 열어서 사용할 수 있어야 하므로, disk space manager의 read/write 함수는 상위 layer인 buffer manager에서 호출될 때 fd값을 인자로 받아서 이에 적합한 동작을 수행한다. 즉 buffer manager가 각 table에 관한 정보를 관리한다.
  • Project2에서는 allocate와 free 기능을 disk space manager가 수행했지만, Project3에서는 disk space manager의 read/write 함수를 활용하여 buffer manager가 allocate와 free 기능을 관리하므로, disk space manager에서는 해당 기능을 하는 함수를 삭제한다.

3. Layer 구조와 함수 상세정보

Index

  1. Layer 구조
  2. API layer
  3. index manager
  4. buffer manager
  5. disk space manager

1. Layer 구조

  • API layer는 과제에서 요구하는 API services를 제공하며, 하위 layer의 wrapper functions로 이루어져 있다.
  • index manager는 project2에서와 유사하게 b+tree의 indexing을 수행한다. 하위 layer인 buffer manager의 wrapper functions 일부와, 필요에 따라 buffer manager 소속 함수를 호출해가며 동작하는 함수들로 이루어져 있다.
  • buffer manager는 project3에서 구현을 요구한 사항으로, on-disk pages를 cashing하기 위해 in-memory buffer를 생성하고 관리한다. 이를 수행하기 위한 구조체와 함수 등으로 이루어져 있다.
  • disk space manager는 on-disk pages, 즉 table file들을 직접적으로 관리한다. 이를 수행하기 위한 구조체와 함수 등으로 이루어져 있다.
  • 전체 layers/functions 순서도 Flowchart_of_Funtions

2. API layer

index manager의 wrapper functions로 이루어져 있다.

int init_db(int buf_num);
int shutdown_db();

int open_table(char* pathname);
int close_table(int table_id);

int db_insert(int table_id, int64_t key, char* value);
int db_find(int table_id, int64_t key, char* ret_val);
int db_delete(int table_id, int64_t key);

3. index manager

index manager는 위의 수정사항 외에는 project2에서와 매우 유사하게 동작하며 b+tree의 indexing을 수행한다. 하위 layer인 buffer manager의 wrapper functions 일부와, 필요에 따라 buffer manager 소속 함수를 호출해가며 동작하는 함수들로 이루어져 있다.


4. buffer manager

buffer manager는 on-disk pages를 cashing하기 위해 in-memory buffer를 생성하고 관리한다. 이를 수행하기 위한 구조체와 함수 등으로 이루어져 있다.



buffer_block 구조체

typedef struct buffer_block buffer_block_t;
struct buffer_block {
    // Buffer page frame.
    page_t frame;

    // Buffer control block.
    int table_id;
    pagenum_t page_num;
    bool is_dirty;
    bool is_pinned;
    buffer_block_t* next_of_LRU;
    buffer_block_t* prev_of_LRU;
    buffer_block_t* next_of_table;
    buffer_block_t* prev_of_table;
};
  • on-disk page를 memory buffer에 올리기 위해 사용하는 buffer_block 구조체를 정의한다.
  • 과제에서 구현을 요구한 fields를 구현한다.
    • frame : 해당 page(4096 bytes)를 contain.
    • table_id : 해당 page가 속한 table의 id.
    • page_num : 해당 page의 page number.
    • is_dirty : read한 후 write 여부.
    • is_pinned : 현재 사용 중 여부.
    • next_of_LRU/prev_of_LRU : LRU policy에 따라 buffer를 관리하기 위해 buffer_block들을 2 way linked list로 연결한다. 이러한 LRU list의 구현을 위해 buffer_block 구조체가 가져야할 포인터.
  • next_of_table/prev_of_table : table별로 buffer_block들을 2 way linked list로 연결하여, 특정 table의 page를 탐색할 때 사용한다. 선형적으로 탐색하므로 buffer와 buffer에 올라간 page 수가 증가할수록 느려지는 미흡한 성능을 보여주고 있다. 따라서 hash 등의 방법으로 교체하여 개선할 필요가 있다.


buffer_table 구조체

typedef struct buffer_table buffer_table_t;
struct buffer_table {
    int table_id;
    int fd;
    int block_num;
    buffer_block_t* head_of_table;
};
  • 최대 10개의 table을 동시에 열어 관리하기 위해 각각의 table 정보를 저장하는 데에 사용하는 buffer_table 구조체를 정의한다.
  • table_id : 해당 table의 unique id. buffer manager 내부와 상위 layer에서 각각의 table을 관리하는 데에 사용한다.
  • fd : 해당 table의 file descriptor 값. disk space manager를 통해 실제 table file을 read/write할 때 사용한다.
  • block_num : 현재 buffer에 올라간 해당 table의 page 수. 추후 buffer_block 관리 방법이 변경될 시 삭제될 수 있다.
  • head_of_table : 해당 table 소속 buffer_block linked list의 head. 추후 buffer_block 관리 방법이 변경될 시 삭제될 수 있다.


전역 변수

bool db_opened = false;
int buffer_size = 0;
int table_num = 0;
int block_num = 0;
buffer_table_t buf_tables[MAX_NUM_OF_TABLE_ID];
buffer_block_t* head_of_LRU_list = NULL;
buffer_block_t* tail_of_LRU_list = NULL;
buffer_block_t* buf_pool = NULL;
  • buffer 관리를 위한 buffer manager layer의 전역 변수들을 선언한다.
  • db_opened : 현재 db의 initialize / shutdown 여부를 저장한다. 초기값으로 false를 가진다.
  • buffer_size : db initialize 시 buffer pool의 size, 즉 buffer block의 최대 개수를 입력받아 저장한다.
  • table_num : 현재 열려 있는 table의 개수를 저장한다.
  • block_num : 현재 on-disk page가 올라와있는 buffer_block의 개수를 저장한다.
  • buf_tables[] : 여러 개(최대 10개)의 table을 동시에 사용하기 위해 buffer_table 구조체의 배열을 사용한다.
  • head_of_LRU_list / tail_of_LRU_list : LRU policy에 따라 buffer를 관리하기 위해 linked list인 LRU list를 사용한다.
  • buf_pool : buffer_block의 배열인 buffer pool. 포인터로 선언하고 db initialize 시 입력받은 buffer_size에 맞게 동적할당한다.


find_table_index

int find_table_index(int table_id);

buf_tables 배열에서 특정 id를 가지는 table의 index를 탐색한다.


  • db가 초기화되지 않았으면 -2를 반환한다.
  • buf_tables 배열을 선형적으로 탐색하여 해당 id를 가지는 table의 index를 탐색하고 그 값을 반환한다.
    • 찾지 못하면 -1을 반환한다.


evict_buffer_block

int evict_buffer_block(buffer_block_t* buf_block);

특정 buffer block을 buffer에서 evict한다.


  1. 해당 buffer block이 write된 상태(is_dirty)일 경우 file_write_page 함수를 call하여 실제 table file에 write한다.
  2. LRU list와 table에서의 buffer block linked list에서 해당 buffer block을 제거한다.
  3. 해당 buffer_block의 table_id 값을 0으로 변경한다. table_id는 1 이상의 값을 가지므로, 0으로 두는 것은 현재 사용하고 있지 않은 buffer_block임을 의미한다.
  4. 관련 변수들을 최신화한다.
  5. 0(성공)을 반환한다.


create_buffer_block

int create_buffer_block(int table_id, pagenum_t pagenum, page_t buf_page, buffer_block_t **dest);

특정 table의 특정 page를 buffer에 올린다.

  • 인자
    • table_id (int) : buffer에 올릴 page가 속한 table의 id.
    • pagenum (pagenum_t) : buffer에 올릴 page의 page number.
    • buf_page (page_t) : buffer에 올릴 page 구조체.
    • dest (buffer_block_t**) : caller가, 해당 page를 올린 buffer_block 포인터를 받을, 포인터 변수의 주소.
  • 반환 : (int)
    • 성공 여부.
  • buffer_request_page 함수에서 호출된다.

  1. buffer pool의 full 여부를 판단한다.
  • full일 경우, LRU policy에 따라 선택된 page를 evict_buffer_block 함수를 call하여 evict하고 빈 buffer block을 얻는다.
  • full이 아닐 경우, 현재 사용하고 있지 않은, 즉 빈 buffer block을 탐색한다.
  1. 해당 buffer block을 입력받은 값 등으로 초기화한다.
  2. 해당 buffer block을 LRU list의 head로 설정한다.
  3. 해당 buffer block을 해당 table의 buffer block linked list에 추가한다.
  4. 관련 변수들을 최신화한다.
  5. 입력받은, caller가, 해당 page를 올린 buffer_block 포인터를 받을, 포인터 변수에 현재 buffer block 포인터 값을 대입하고, 0(성공)을 반환한다.


buffer_init_db

int buffer_init_db(int buf_num);

DB를 초기화, 즉 DBMS의 실행을 시작한다.

  • 인자
    • buf_num (int) : 초기화하는 DBMS가 가질 buffer pool의 size.
  • 반환 : (int)
    • 성공 여부.
  • index manager의 index_init_db 함수(wrapper function)에서 호출된다.

  1. DB가 이미 초기화되어 있으면 -1을 반환한다.
  2. 입력받은 buf_num 개수만큼의 buffer block들로 buffer pool을 동적 할당한다.
  • 할당에 실패하면 -2를 반환한다.
  1. buffer block들의 table_id 값을 0으로, 즉 빈 buffer block으로 초기화한다.
  2. 관련 변수들을 최신화한다.
  3. 0(성공)을 반환한다.


buffer_shutdown_db

int buffer_shutdown_db(void);

DBMS의 실행을 종료한다.

  • 반환 : (int)
    • 성공 여부.
  • index manager의 index_shutdown_db 함수(wrapper function)에서 호출된다.

  1. DB가 초기화되어 있지 않으면, 즉 현재 DBMS가 실행 중이 아니면 -1을 반환한다.
  2. buffer_close_table을 호출하여 모든 table을 닫는다.
  • close table 중 오류 발생 시 -2를 반환한다.
  1. buffer pool의 할당을 해제한다.
  2. 관련 변수를 최신화한다.
  3. 0(성공)을 반환한다.


buffer_open_table

int buffer_open_table(char* pathname);

특정 table을 연다.

  • 인자
    • pathname (char*) : 열고자 하는 table file의 pathname.
  • 반환 : (int)
    • 성공 여부.
  • index manager의 index_open_table 함수(wrapper function)에서 호출된다.

  1. DB가 초기화되어 있지 않으면, 즉 현재 DBMS가 실행 중이 아니면 -1을 반환한다.
  2. 테이블이 최대 개수(현재 10)만큼 열려 있으면 -2를 반환한다.
  3. file_open_table 함수를 call하여 파일을 열고 file descriptor (fd) 값을 받는다.
  • 실패(fd < 0) 시 -3을 반환한다.
  1. 관련 변수와 해당 table에 사용할 buffer_table 구조체를 최신화/초기화한다. table_id는 fd 값 + 1로 한다. (fd 값은 0부터 시작하므로, table_id 값을 1 이상으로 하기 위해서.) (일반적으로 0, 1, 2는 stdin, stdout, stderr의 용도로 사용되어 3부터 시작된다고 볼 수 있으나, 이는 변경될 수 있는 부분이므로.)
  2. table_id를 반환한다.


buffer_close_table

int buffer_close_table(int table_id);

현재 열려 있는 특정 table을 닫는다.

  • 인자
    • table_id (int) : 닫고자 하는 table의 id.
  • 반환 : (int)
    • 성공 여부.
  • buffer_shutdown_db 함수에서 호출된다.
  • index manager의 index_open_table 함수(wrapper function)에서 호출된다.

  1. DB가 초기화되어 있지 않으면, 즉 현재 DBMS가 실행 중이 아니면 -1을 반환한다.
  2. find_table_index 함수를 call하여, buf_tables 배열에서, 입력받은 table_id에 해당하는 table의 index를 확인한다.
  • 해당 table이 존재하지 않을 시 -2를 반환한다.
  1. 해당 table에 속하는 모든 buffer block의 pinned 여부를 확인하고, 없을 시 모두 pin을 꽂는다.
  • pin이 꽂힌 buffer block이 있을 경우 -3을 반환한다. (현재 단일 유저가 close_table을 시도했을 때 pin이 꽂힌 buffer block이 있는 것은 처리가 잘못된 것이므로 에러를 반환하나, 추후 여러 유저가 동시에 접근할 수 있게 될 경우 waiting을 주는 등의 방법으로 교체해야 한다.)
  1. evict_buffer_block 함수를 call하여 해당 table에 속하는 모든 buffer block을 evict한다.
  • evict 중 에러 발생 시 -4를 반환한다.
  1. file_close_table 함수를 call하여 실제 테이블 파일을 닫는다.
  • 실패 시 -5를 반환한다.
  1. buf_tables 배열에서 해당 buffer_table 구조체를 제거한다.
  2. 0(성공)을 반환한다.


buffer_alloc_page

pagenum_t buffer_alloc_page(int table_id);

특정 table에 새로운 page를 allocate한다.

  • 인자
    • table_id (int) : 새로운 page를 allocate할 table의 id.
  • 반환 : (pagenum_t)
    • 할당된 page의 page number. page number는 0부터 시작하므로, 음수를 error value로 사용한다.
  • index manager의 index_start_new_tree, index_insert_into_leaf_after_splitting, index_insert_into_new_root, index_insert_into_internal_after_splitting 함수에서 호출된다.

  1. free page 존재 여부를 확인한다.
  • 존재하지 않을 시, 파일 끝에 새로운 page를 생성한다. 실제 file write가 이루어진다.
  • 존재할 시, free page list에서 1개 page를 추출하여 사용하도록 한다.
  • 이 과정 중 file_write_page 함수와 buffer_request_page 함수가 사용된다. 해당 함수들에서 오류 발생 시, 음수값을 반환한다.
  1. 할당된 page의 page number를 반환한다.


buffer_free_page

int buffer_free_page(int table_id, pagenum_t pagenum);

특정 table의 특정 page를 free한다.

  • 인자
    • table_id (int) : 특정 page를 free할 table의 id.
    • pagenum (pagenum_t) : free할 page의 page number.
  • 반환 : (int)
    • 성공 여부.
  • index manager의 index_remove_entry_from_internal, index_adjust_root 함수에서 호출된다.

  1. 입력받은 page를 free page list에 추가한다.
  1. 관련 변수들을 최신화한다.
  2. 0(성공)을 반환한다.


buffer_request_page

int buffer_request_page(int table_id, pagenum_t pagenum, buffer_block_t** dest);

상위 layer의 page 요청에 응한다.

  • 인자
    • table_id (int) : 요청하는 page가 속한 table의 id.
    • pagenum (pagenum_t) : 요청하는 page의 page number.
    • dest (buffer_block_t**) : caller가, 해당 page의 buffer_block 포인터를 받을, 포인터 변수의 주소.
  • 반환 : (int)
    • 성공 여부.
  • buffer_alloc_page, buffer_free_page 함수에서 호출된다.
  • index manager의 index_find, index_find_leaf, index_insert, index_start_new_tree, index_insert_into_leaf_after_splitting, index_insert_into_parent, index_insert_into_new_root, index_insert_into_internal_after_splitting, index_delete_record, index_delete_entry, index_remove_entry_from_internal, index_adjust_root, index_merge_pages, index_redistribute_pages, index_find_left_leaf 함수에서 호출된다.

  1. DB가 초기화되어 있지 않으면, 즉 현재 DBMS가 실행 중이 아니면 -1을 반환한다.
  2. 해당 table이 열려있지 않으면 -2를 반환한다.
  3. 해당 table의 buffer block linked list에서 해당 page를 탐색한다.
  • 존재하지 않을 시, file_read_page 함수를 call하여 table 파일에서 해당 page를 읽고, create_buffer_block 함수를 call하여 이를 buffer에 올린다. 이 과정 중 오류 발생 시 음수를 반환한다.
  1. 해당 buffer에 pin을 꽂는다.
  2. 입력받은, caller가, 해당 page의 buffer_block 포인터를 받을, 포인터 변수에 해당 buffer block 포인터 값을 대입하고, 0(성공)을 반환한다.


buffer_release_page

int buffer_release_page(buffer_block_t* buf_block, bool is_writed);

상위 layer의 page 해제 요청에 응한다.

  • 인자
    • buf_block (buffer_block_t*) : 해제할 page의 buffer block 구조체 포인터.
    • is_writed (bool) : request 되어 지금 release 될 때까지 page에 수정이, 즉 write가 이루어졌는지 여부.
  • 반환 : (int)
    • 성공 여부.
  • buffer_alloc_page, buffer_free_page 함수에서 호출된다.
  • index manager의 index_find, index_find_leaf, index_start_new_tree, index_insert_into_leaf, index_insert_into_parent, index_insert_into_new_root, index_insert_into_internal, index_insert_into_internal_after_splitting, index_delete_record, index_delete_entry, index_remove_entry_from_internal, index_adjust_root, index_merge_pages, index_redistribute_pages, index_find_left_leaf 함수에서 호출된다.

  1. 해당 buffer block에 pin이 꽂혀있지 않으면 -1을 반환한다. (caller에서 해당 page를 request하여 pin을 꽂고 사용하다가 이를 마치고 unpin하기 위해 release하는데, unpin된 상태의 buffer block 입력은 비정상적인 상황이다.)
  2. is_writed 인자가 true이면 buffer block의 is_dirty 값을 true로 변경한다.
  3. 해당 buffer block을 LRU list의 head가 되도록 한다.
  4. pin을 제거하고 0(성공)을 반환한다.

5. disk space manager

disk space manager는 위의 [수정사항](# 4-disk-space manager-수정) 외에는 project2에서와 매우 유사하게 동작하며 on-disk pages, 즉 table file들을 직접적으로 관리한다. 이를 수행하기 위한 구조체와 함수 등으로 이루어져 있다.


4. Todo

1. buffer pool 탐색 방식 변경

  • 현재 구현한 buffer manager는 table별로 linked list를 만들어서 각 buffer block을 관리하고 있다.
  • buffer block의 수가 증가하면, 매 request마다 해당 table에서 특정 page를 탐색할 때 linked list를 선형적으로 탐색하면서 많은 delay가 발생한다.
  • 실제로 10만 / 100만 개의 중복 없는 랜덤 insert / find / delete 테스트를 하였을 때, buffer block 수 400 > 1000 > 4000 개 순으로 실행 속도가 빠름을 확인하였다. 즉 미흡한 구현으로 인해 buffer가 클수록 속도가 느려지는 비정상적인 상태이다.
  • 따라서, hash 등의 방법을 사용하여 buffer pool 탐색 속도를 개선할 필요가 있다.
  • 또한, buffer에 빈 공간이 있을 때 request에서 disk read를 위해 빈 buffer block을 찾아야 하는데, 현재 빈 공간을 찾을 때 buffer pool array를 선형적으로 탐색하고 있으므로 이 부분도 개선이 필요하다.

2. index_find 탐색 방식 변경

  • 현재 구현된 index_find_leaf와 index_find 함수는 특정 key를 가지고 entry / record 탐색 시 선형적으로 탐색하고 있다.
  • B+tree는 정렬된 형태이므로, binary search 등의 방법을 통해 탐색 속도을 개선할 수 있다.

3. Merge / Redistribute 우선순위 판단

  • 현재 merge를 최대한 줄이기 위해 redistribute을 우선으로 실행하고 있다.
  • 하지만 merge보다 redistribute의 반복이 I/O를 더 많이 발생시킬 수 있으므로, 이를 확인해보고 무엇을 우선으로 할 때 더 효율적일지 판단하여 적용해야 한다.
  • 현재 구현은 redistribute을 우선으로 한다는 가정 하에 이루어져 있으므로, merge 우선을 선택할 시 merge / redistribute 함수의 수정이 필요하다.

4. 에러 체크

  • 현재 각각의 함수에서 에러 발생 시 그에 대한 적절한 조치 없이 단순히 음수값을 반환하는 등 에러 체크가 미흡한 상태이다.
  • 추후 recovery를 적용할 것을 고려하며 함수들의 각 위치에서 에러가 발생했을 때 해당 transaction의 수행을 취소하고 그 전 상태로 온전히 복구하는 등의 조치가 필요하다.

5. 불필요한 보조함수 정리

  • 현재 서로 유사한 기능을 가진 함수들 / 특정 기능을 불필요하게 여러 함수로 나누어 수행하는 경우 등이 존재하므로 정리가 필요하다.
Clone this wiki locally