Compare commits

..

4 Commits

Author SHA1 Message Date
oschly 29dc16413e remove headers from page breaks 2026-06-07 17:46:42 +02:00
oschly 1bd2d08677 styling fixes
- include bib in contents
- fix page breaks between chapters
- fix colors in table of contents
2026-06-07 17:19:49 +02:00
oschly a525fe60dd typo, hide counter on empty pages, add short title 2026-06-07 16:22:00 +02:00
oschly 334776da5a split the code to fix rendering issues 2026-06-06 20:40:58 +02:00
8 changed files with 66 additions and 70 deletions
+2 -2
View File
@@ -43,7 +43,7 @@
#let render-bib() = { #let render-bib() = {
load-bibliography("Bibliography.bib", prefix: "cite:", full: true, style: "ieee") load-bibliography("Bibliography.bib", prefix: "cite:", full: true, style: "ieee")
heading(level: 1, outlined: false)[Spis literatury] heading(level: 1, outlined: true, numbering: none)[Spis literatury]
v(1em) v(1em)
context { context {
@@ -180,7 +180,7 @@
let render-group(title, keys, start-idx) = { let render-group(title, keys, start-idx) = {
if keys.len() > 0 [ if keys.len() > 0 [
#heading(level: 2, title, outlined: false) #heading(level: 2, title, outlined: true, numbering: none)
#grid( #grid(
columns: (auto, 1fr), columns: (auto, 1fr),
column-gutter: 0.65em, column-gutter: 0.65em,
+1 -1
View File
@@ -4,7 +4,7 @@
== Systemy współdzielenia dokumentów w czasie rzeczywistym == Systemy współdzielenia dokumentów w czasie rzeczywistym
Budując rozwiązania związane z równoczesnym tworzeniem i modyfikacją tekstu przez więcej niż jednego użytkownika, musimy rozważyć wyzwania napotykane w aktualizacji tworzonego dokumentu, gdzie każdy klient posiada lokalną kopię i nanosi na nie własne zmiany, ale też w międzyczasie musimy nanieść zmiany od pozostałych klientów. W takim systemie mówimy wtedy o zbieżności danych@cite:eventually_consistent - czyli zapewnieniu tego samego stanu między każdym klientem. W przypadku edycji tekstu skupię się na ewentualnej zbieżności, która uwzględnia posiadanie rozbieżnych kopii tego samego źródła danych u każdego z klientów przez pewien czas. Dopiero gdy zostanie zakończona edycja tekstu, zmiany zostają propagowane i nanoszone do pozostałych klientów. Finalnie każdy klient po czasie posiada identyczną kopię dokumentu. Ze strony doświadczeń użytkowania jest to skuteczna strategia ze względu na możliwość zapewnienia płynności interfejsu graficznego oraz z pomocą złożonych mechanizmów umożliwia rozwiązywanie konfliktów między kopiami. Budując rozwiązania związane z równoczesnym tworzeniem i modyfikacją tekstu przez więcej niż jednego użytkownika, musimy rozważyć wyzwania napotykane w aktualizacji tworzonego dokumentu, gdzie każdy klient posiada lokalną kopię i nanosi na nie własne zmiany, ale też w międzyczasie musimy nanieść zmiany od pozostałych klientów. W takim systemie mówimy wtedy o zbieżności danych@cite:eventually_consistent - czyli zapewnieniu tego samego stanu między każdym klientem. W przypadku edycji tekstu skupię się na ewentualnej zbieżności, która uwzględnia posiadanie rozbieżnych kopii tego samego źródła danych u każdego z klientów przez pewien czas. Dopiero gdy zostanie zakończona edycja tekstu, zmiany zostają propagowane i nanoszone do pozostałych klientów. Finalnie każdy klient po czasie posiada identyczną kopię dokumentu. Ze strony doświadczeń użytkowania jest to skuteczna strategia ze względu na możliwość zapewnienia płynności interfejsu graficznego oraz z pomocą złożonych mechanizmów umożliwia rozwiązywanie konfliktów między kopiami.
Wspomniany model nie jest bez wad. Największym problemem jest istnienie konfliktów, których rozwiązanie klienci muszą ustalić za pomocą dodatkowych strategii. Najeczęściej wykorzystywaną jest Last-Write-Wins (LWW). Rozstrzyga ona konflikty poprzez nanoszenie tylko tej zmiany, która jest uznawana jako ostatnia w kolejności zbioru konfliktujących operacji. Ustalanie kolejności nie jest jasno tutaj zdefiniowane. W systemach baz danych takich jak Cassandra@cite:apache_cassandra_documentation oraz SQL Server P2P@cite:microsoft_sql_server_p2p_replication_documentation każdy zapis otrzymuje własny znacznik czasowy, na podstawie którego wybierany jest najmłodszy wpis i nim nadpisywane zmiany w źródle danych. Zmiany ze starszymi znacznikami porzucane. Zauważalną wadą LWW jest wysokie ryzyko utraty danych w czasie nanoszenia zmian, ponieważ wszystkie konfliktujące starsze zmiany nie brane pod uwagę. Wspomniany model nie jest bez wad. Największym problemem jest istnienie konfliktów, których rozwiązanie klienci muszą ustalić za pomocą dodatkowych strategii. Najczęściej wykorzystywaną jest Last-Write-Wins (LWW). Rozstrzyga ona konflikty poprzez nanoszenie tylko tej zmiany, która jest uznawana jako ostatnia w kolejności zbioru konfliktujących operacji. Ustalanie kolejności nie jest jasno tutaj zdefiniowane. W systemach baz danych takich jak Cassandra@cite:apache_cassandra_documentation oraz SQL Server P2P@cite:microsoft_sql_server_p2p_replication_documentation każdy zapis otrzymuje własny znacznik czasowy, na podstawie którego wybierany jest najmłodszy wpis i nim nadpisywane zmiany w źródle danych. Zmiany ze starszymi znacznikami porzucane. Zauważalną wadą LWW jest wysokie ryzyko utraty danych w czasie nanoszenia zmian, ponieważ wszystkie konfliktujące starsze zmiany nie brane pod uwagę.
W przypadku tekstu jako typu danych, istnieje specjalny wariant ewentualnej zbieżności - silna ewentualna zbieżność. Ten model wykorzystuje specjalne struktury danych, które zapewniają bezkonfliktowe nanoszenie zmian, a ich skuteczność opiera się na matematycznych dowodach@cite:verifying_strong_eventual_consistency. W przypadku tekstu jako typu danych, istnieje specjalny wariant ewentualnej zbieżności - silna ewentualna zbieżność. Ten model wykorzystuje specjalne struktury danych, które zapewniają bezkonfliktowe nanoszenie zmian, a ich skuteczność opiera się na matematycznych dowodach@cite:verifying_strong_eventual_consistency.
+1 -1
View File
@@ -122,7 +122,7 @@ struct NoteInvitation: Identifiable {
caption: [Definicja obiektu reprezentującego zaproszenie do edycji notatki], caption: [Definicja obiektu reprezentującego zaproszenie do edycji notatki],
) )
Ostatnim obiektem jest struktura `Peer` przedstawiona w programie 3.4, która jest opisem aktualnego stanu połączenia wykrytego innej instancji systemu w pobliżu użytkownika. Składa się z wartości enumerowanej opisującej stan połączeniao raz identyfikatorem użytkownika w sieci peer to peer. Implementuja ona protokół `Idenfitiable`, by móc zostać poprawnie użyta do rysowania listy dostępnych klientów w pobliżu użytkownika. Ostatnim obiektem jest struktura `Peer` przedstawiona w programie 3.4, która jest opisem aktualnego stanu połączenia wykrytego innej instancji systemu w pobliżu użytkownika. Składa się z wartości enumerowanej opisującej stan połączenia wraz z identyfikatorem użytkownika w sieci peer to peer. Implementuje ona protokół `Identifiable`, by móc zostać poprawnie użyta do rysowania listy dostępnych klientów w pobliżu użytkownika.
#let peer_struct = [```swift #let peer_struct = [```swift
struct Peer: Identifiable { struct Peer: Identifiable {
+9 -7
View File
@@ -53,7 +53,7 @@ final class InMemoryStorageProvider: StorageProvider {
Na bazie powyższego kodu zbudowałem pięć przypadków testowych klasy `NotesStorageTests` (program 4.2). Pierwszy z nich weryfikuje, że przy pustym katalogu metoda `loadNotes()` zwraca pustą tablicę. Kolejne dwa testy sprawdzają poprawność tworzenia pliku oraz odczytu istniejącej notatki. Ostatni test sprawdza, czy dwukrotne wywołanie metody tworzącej notatkę o tej samej nazwie generuje dwa odrębne obiekty. Na bazie powyższego kodu zbudowałem pięć przypadków testowych klasy `NotesStorageTests` (program 4.2). Pierwszy z nich weryfikuje, że przy pustym katalogu metoda `loadNotes()` zwraca pustą tablicę. Kolejne dwa testy sprawdzają poprawność tworzenia pliku oraz odczytu istniejącej notatki. Ostatni test sprawdza, czy dwukrotne wywołanie metody tworzącej notatkę o tej samej nazwie generuje dwa odrębne obiekty.
#let notes_storage_tests = [```swift #let notes_storage_tests_part_1 = [```swift
@Suite @Suite
struct NotesStorageTests { struct NotesStorageTests {
private func makeStorage(root: URL = URL(filePath: "/test")) -> (NotesStorage, InMemoryStorageProvider) { private func makeStorage(root: URL = URL(filePath: "/test")) -> (NotesStorage, InMemoryStorageProvider) {
@@ -99,7 +99,9 @@ struct NotesStorageTests {
let paths = provider.files.keys let paths = provider.files.keys
#expect(paths.contains(where: { $0.contains("Note 1") })) #expect(paths.contains(where: { $0.contains("Note 1") }))
} }
```]
#let notes_storage_tests_part_2 = [```swift
@Test @Test
func createNoteTwiceCreatesTwoFiles() { func createNoteTwiceCreatesTwoFiles() {
let root = URL(filePath: "/test") let root = URL(filePath: "/test")
@@ -111,9 +113,8 @@ struct NotesStorageTests {
} }
} }
```] ```]
#figure( #figure(
notes_storage_tests, [#notes_storage_tests_part_1, #notes_storage_tests_part_2],
kind: raw, kind: raw,
caption: [Testy jednostkowe dla NotesStorage], caption: [Testy jednostkowe dla NotesStorage],
) )
@@ -195,7 +196,7 @@ struct NoteContentCodableTests {
`NoteInvitationTests` (program 4.5) obejmuje siedem przypadków testowych. Dwa pierwsze weryfikują, czy metody `accept()` oraz `decline()` przekazują odpowiednio wartości logiczne `true` i `false` do handlera. Kolejne dwa testy sprawdzają czy wielokrotne wywołanie tej samej metody nie powoduje powtórnego wywołania handlera. Piąty test gwarantuje, że po zaakceptowaniu zaproszenia próba jego odrzucenia jest ignorowana. Ostatnie dwa testy weryfikują poprawność obliczanych właściwości: `noteName` zwraca tytuł notatki, a `id` jest tożsame z identyfikatorem nadawcy (`MCPeerID`). `NoteInvitationTests` (program 4.5) obejmuje siedem przypadków testowych. Dwa pierwsze weryfikują, czy metody `accept()` oraz `decline()` przekazują odpowiednio wartości logiczne `true` i `false` do handlera. Kolejne dwa testy sprawdzają czy wielokrotne wywołanie tej samej metody nie powoduje powtórnego wywołania handlera. Piąty test gwarantuje, że po zaakceptowaniu zaproszenia próba jego odrzucenia jest ignorowana. Ostatnie dwa testy weryfikują poprawność obliczanych właściwości: `noteName` zwraca tytuł notatki, a `id` jest tożsame z identyfikatorem nadawcy (`MCPeerID`).
#let note_invitation_tests = [```swift #let note_invitation_tests_part_1 = [```swift
@Suite @Suite
struct NoteInvitationTests { struct NoteInvitationTests {
@Test @Test
@@ -246,7 +247,8 @@ struct NoteInvitationTests {
let invitation = NoteInvitation { _ in } let invitation = NoteInvitation { _ in }
#expect(invitation.noteName == "Shared note") #expect(invitation.noteName == "Shared note")
} }
```]
#let note_invitation_tests_part_2 = [```swift
@Test @Test
func idIsInvitatorPeerID() { func idIsInvitatorPeerID() {
let peerID = MCPeerID(displayName: "host") let peerID = MCPeerID(displayName: "host")
@@ -260,7 +262,7 @@ struct NoteInvitationTests {
```] ```]
#figure( #figure(
note_invitation_tests, [#note_invitation_tests_part_1, #note_invitation_tests_part_2],
kind: raw, kind: raw,
caption: [Testy jednostkowe NoteInvitation], caption: [Testy jednostkowe NoteInvitation],
) )
@@ -287,6 +289,6 @@ Weryfikacja wymagań niefunkcjonalnych została uzupełniona o pomiary wydajnoś
Średnie zużycie pamięci RAM przy uruchomionej aplikacji, bez aktywnej sesji edycji, wynosiło 20 MB. Podczas aktywnej sesji peer-to-peer z dwoma połączonymi klientami zużycie wzrastało do 30 MB. Wzrost ten jest spowodowany głównie utrzymywaniem obiektów związanych z sesją `MCSession` oraz obsługą komponentu edycji tekstu `TextEditor`. Nie zaobserwowano wycieków pamięci w trakcie użytkowania. Średnie zużycie pamięci RAM przy uruchomionej aplikacji, bez aktywnej sesji edycji, wynosiło 20 MB. Podczas aktywnej sesji peer-to-peer z dwoma połączonymi klientami zużycie wzrastało do 30 MB. Wzrost ten jest spowodowany głównie utrzymywaniem obiektów związanych z sesją `MCSession` oraz obsługą komponentu edycji tekstu `TextEditor`. Nie zaobserwowano wycieków pamięci w trakcie użytkowania.
Obciążenie procesora w stanie spoczynku wynosiło 0-1%, gdzie przedział maksymalny wynosi 0-600%, ze względu na obecność 6 rdzenii, których zużycie, w zakresie 0-100%, się sumuje. Podczas intensywnej edycji tekstu z jednoczesną synchronizacją obciążenie wzrastało do 30-40% na urządzeniu iPhone 16 Pro. Głównym źródłem obciążenia była obsługa edycji tekstu oraz wysyłanie danych przez framework Multipeer Connectivity. Obciążenie procesora w stanie spoczynku wynosiło 0-1%, gdzie przedział maksymalny wynosi 0-600%, ze względu na obecność 6 rdzeni, których zużycie, w zakresie 0-100%, się sumuje. Podczas intensywnej edycji tekstu z jednoczesną synchronizacją obciążenie wzrastało do 30-40% na urządzeniu iPhone 16 Pro. Głównym źródłem obciążenia była obsługa edycji tekstu oraz wysyłanie danych przez framework Multipeer Connectivity.
Podsumowując - przeprowadzone testy jednostkowe, scenariuszowe oraz wydajnościowe potwierdzają, że zaimplementowana aplikacja spełnia przyjęte wymagania funkcjonalne i niefunkcjonalne. Architektura oparta na protokole `StorageProvider` umożliwiła efektywne testowanie logiki przechowywania, natomiast manualne scenariusze wykazały stabilność komunikacji P2P w typowych warunkach użytkowania. Podsumowując - przeprowadzone testy jednostkowe, scenariuszowe oraz wydajnościowe potwierdzają, że zaimplementowana aplikacja spełnia przyjęte wymagania funkcjonalne i niefunkcjonalne. Architektura oparta na protokole `StorageProvider` umożliwiła efektywne testowanie logiki przechowywania, natomiast manualne scenariusze wykazały stabilność komunikacji P2P w typowych warunkach użytkowania.
BIN
View File
Binary file not shown.
+7 -4
View File
@@ -11,14 +11,17 @@
#pagebreak() #pagebreak()
#include "Abstract.typ" #include "Abstract.typ"
#pagebreak(to: "odd")
#zut-template([ #zut-template(
[
#include "Chapters/0. Introduction.typ" #include "Chapters/0. Introduction.typ"
#include "Chapters/1. Theoretical Scope.typ" #include "Chapters/1. Theoretical Scope.typ"
#include "Chapters/2. Implementation.typ" #include "Chapters/2. Implementation.typ"
#include "Chapters/3. Tests.typ" #include "Chapters/3. Tests.typ"
#include "Chapters/4. Summary.typ" #include "Chapters/4. Summary.typ"
#render-bib() #render-bib()
#pagebreak(to: "odd") #pagebreak()
#list-of-figures() #list-of-figures()
]) ],
shorttitle: "Aplikacja do współtworzenia notatek z wykorzystaniem technologii peer-to-peer",
)
+18 -33
View File
@@ -79,7 +79,7 @@
margin: ( margin: (
top: 3.2cm, top: 3.2cm,
bottom: 3.5cm, bottom: 3.5cm,
inside: 3.5cm, // 2.5cm tekst + 1cm bindingoffset (rozwiązuje problem marginesu z lewej z Twojego kodu) inside: 3.5cm, // 2.5cm tekst + 1cm
outside: 2.5cm, outside: 2.5cm,
), ),
@@ -88,11 +88,22 @@
let pg = counter(page).get().first() let pg = counter(page).get().first()
let is-odd = calc.odd(pg) let is-odd = calc.odd(pg)
// Czy ta strona otwiera rozdział? (brak nagłówka) // is chapter's first page?
let chapter-locs = query(heading.where(level: 1)) let chapter-locs = query(heading.where(level: 1))
let is-chapter-page = chapter-locs.any(h => counter(page).at(h.location()).first() == pg) let is-chapter-page = chapter-locs.any(h => counter(page).at(h.location()).first() == pg)
if not is-chapter-page { // Detect blank pages inserted by pagebreak(to: "odd").
// The marker is placed before the pagebreak, so it lands on the last page
// of the previous chapter (pg - 1 relative to the blank even page).
let chapter-break-markers = query(<chapter-break-marker>)
let is-blank-page = (
not is-odd
and chapter-break-markers.any(
m => counter(page).at(m.location()).first() == pg - 1,
)
)
if chapter-locs.len() == 0 {} else if not is-chapter-page and not is-blank-page {
// Tytuł bieżącego rozdziału do nagłówka // Tytuł bieżącego rozdziału do nagłówka
let prev = query(heading.where(level: 1).before(here())) let prev = query(heading.where(level: 1).before(here()))
let ch-label = if prev.len() > 0 { let ch-label = if prev.len() > 0 {
@@ -174,7 +185,10 @@
if it.level == 1 { if it.level == 1 {
// Wymuszenie nowej strony dla głównego rozdziału // Wymuszenie nowej strony dla głównego rozdziału
pagebreak(weak: true) // Marker placed before the break so it appears on the last page of the
// previous chapter; used by the header to detect blank even pages.
[#metadata("chapter-break") <chapter-break-marker>]
pagebreak(to: "odd", weak: true)
v(1.5em, weak: true) v(1.5em, weak: true)
block(width: 100%, inset: (bottom: 1em, top: 7em))[ block(width: 100%, inset: (bottom: 1em, top: 7em))[
@@ -207,35 +221,6 @@
} }
} }
// ── 3.5 SPIS TREŚCI ─────────────────────────────────────
set outline(indent: auto)
show outline.entry: it => {
if it.level == 1 {
v(12pt, weak: true)
text(size: 11pt, weight: "bold", font: "Fira Sans", fill: blue-zut)[
#h(1.25cm)
#it
]
} else if it.level == 2 {
v(3pt, weak: true)
text(size: 10pt, font: "Fira Sans")[
#h(1.25cm)
#it
]
} else if it.level == 3 {
v(1pt, weak: true)
text(size: 9pt, font: "Fira Sans")[
#h(2.5cm)
#it
]
} else {
it
}
}
// ── 3.6 BLOKI KODU ────────────────────────────────────── // ── 3.6 BLOKI KODU ──────────────────────────────────────
show raw.where(block: true): it => { show raw.where(block: true): it => {
block( block(
+9 -3
View File
@@ -6,7 +6,9 @@
#set heading(numbering: "1.1") #set heading(numbering: "1.1")
#set text(font: "Fira Sans") #set text(font: "Fira Sans")
#set outline(title: text(size: 24pt, fill: color-dark-blue, weight: "regular")[Spis treści]) #set outline(title: block(text(size: 24pt, fill: color-dark-blue, weight: "regular")[Spis treści], inset: (
"bottom": 1.5em,
)))
#show outline.entry: it => { #show outline.entry: it => {
let is-lvl-1 = it.level == 1 let is-lvl-1 = it.level == 1
@@ -27,9 +29,13 @@
text(size: 12pt, fill: black, title) text(size: 12pt, fill: black, title)
} }
let page-style = text(size: 10pt, fill: color-page-num, weight: "medium", page-num) let page-style = if is-lvl-1 {
text(size: 10pt, fill: color-page-num, weight: "medium", page-num)
} else {
text(size: 10pt, fill: black, weight: "medium", page-num)
}
v(if is-lvl-1 { 14pt } else { 6pt }, weak: true) v(if is-lvl-1 { 18pt } else { 10pt }, weak: true)
link(it.element.location())[ link(it.element.location())[
#grid( #grid(