diff --git a/development/github_pages/GITHUB_PAGES_PLAN.md b/development/github_pages/GITHUB_PAGES_PLAN.md new file mode 100644 index 0000000..9b07f1f --- /dev/null +++ b/development/github_pages/GITHUB_PAGES_PLAN.md @@ -0,0 +1,194 @@ +# Timer Ninja — GitHub Pages Planning Document + +## Overview + +A Jekyll-based GitHub Pages site serving as both a visually striking landing page and full documentation hub for the Timer Ninja library. Deployed from the `docs/` folder on the `main` branch. + +--- + +## Decisions + +| Decision | Choice | +|---|---| +| **Tech stack** | Jekyll (GitHub Pages native, no build CI needed) | +| **Theme** | Fully custom (no base theme) for max creative control | +| **Page scope** | Multi-page: Landing + User Guide + Examples + Advanced Usage | +| **Navigation** | Sticky top navbar with logo, page links, GitHub link, day/night toggle | +| **Hero style** | Full-screen with animated gradient/particle background + ninja sloth mascot | +| **Code comparison** | Side-by-side panels (Traditional vs Timer Ninja) | +| **Animations** | Rich — parallax hero, scroll-triggered fade/slide, animated trace output, typing code effect | +| **Mascot usage** | Heavy — hero, CTA section, footer, 404 page | +| **Day/night mode** | CSS custom properties + JS toggle, preference saved in localStorage | +| **Primary color** | `#46bfc6` (light blue) with complementary palette | + +--- + +## Design System + +### Color Palette + +**Light Mode:** +| Token | Value | Usage | +|---|---|---| +| Primary | `#46bfc6` | Brand color, buttons, links, accents | +| Primary Dark | `#3aa3a9` | Hover states | +| Primary Light | `#6dd5db` | Gradients, glow | +| Background | `#ffffff` | Page background | +| Surface | `#f5fafa` | Section backgrounds | +| Text | `#1a2b3c` | Body text | +| Text Muted | `#5a6b7c` | Secondary text | +| Code BG | `#f0f6f6` | Code block backgrounds | + +**Dark Mode:** +| Token | Value | Usage | +|---|---|---| +| Primary | `#46bfc6` | Unchanged | +| Primary Light | `#6dd5db` | Highlighted elements | +| Background | `#0d1520` | Page background | +| Surface | `#14202e` | Section backgrounds | +| Text | `#e8f0f2` | Body text | +| Text Muted | `#8fa3b2` | Secondary text | +| Code BG | `#111d2b` | Code block backgrounds | + +### Typography +- **Body:** Inter (Google Fonts) +- **Code:** JetBrains Mono (Google Fonts) + +--- + +## Site Structure + +``` +docs/ +├── _config.yml # Jekyll configuration +├── _data/ +│ └── navigation.yml # Navbar links +├── _includes/ +│ ├── head.html # Meta, fonts, CSS, theme init script +│ ├── navbar.html # Sticky navbar with theme toggle +│ └── footer.html # Footer with mascot +├── _layouts/ +│ ├── default.html # Base layout +│ ├── home.html # Landing page layout (includes particles + typing JS) +│ └── docs.html # Documentation layout (sidebar TOC + scrollspy) +├── _sass/ +│ ├── _variables.scss # Design tokens, CSS custom properties +│ ├── _base.scss # Reset, typography, global styles +│ ├── _navbar.scss # Sticky nav, hamburger menu +│ ├── _hero.scss # Hero section, float animation +│ ├── _features.scss # Feature cards grid +│ ├── _code.scss # Code panels, trace output, quickstart steps, tabs +│ ├── _docs.scss # Documentation sidebar + content styles +│ ├── _animations.scss # Keyframes, scroll-triggered classes +│ ├── _footer.scss # Footer styles +│ └── _dark-mode.scss # Dark mode overrides +├── assets/ +│ ├── css/main.scss # SCSS entry point +│ ├── js/ +│ │ ├── theme-toggle.js # Day/night mode + hamburger + nav scroll +│ │ ├── animations.js # IntersectionObserver scroll reveals + tabs + trace +│ │ ├── particles.js # Canvas particle system for hero +│ │ ├── typing-effect.js # Typing animation for code comparison +│ │ └── docs-toc.js # Auto-generated TOC + scrollspy for docs +│ └── images/ +│ └── mascot.png # Ninja sloth mascot +├── index.html # Landing page +├── user-guide.md # User Guide (from wiki) +├── examples.md # Examples (from wiki) +├── advanced-usage.md # Advanced Usage (from wiki) +├── 404.html # Custom 404 page +└── Gemfile # Jekyll dependencies +``` + +--- + +## Landing Page Sections + +1. **Hero** — Full-screen animated gradient with canvas particles, floating mascot, tagline, CTA buttons, version badge +2. **Why Timer Ninja?** — 6 feature cards in responsive grid: One Annotation, Visual Call Tree, Block Tracking, Smart Thresholds, Zero Dependencies, Thread-Safe +3. **Before & After** — Side-by-side code comparison with typing animation: 6 lines of boilerplate → 1 annotation +4. **See It In Action** — Terminal-style trace output with line-by-line reveal animation +5. **Quick Start** — 4-step guide with tabbed Maven/Gradle code blocks +6. **Block Tracking Highlight** — Dedicated showcase of `TimerNinjaBlock.measure()` API +7. **CTA** — Final call-to-action with mascot + +--- + +## Documentation Pages + +| Page | Source | Layout | +|---|---|---| +| User Guide | `wiki/User-Guide.md` | `docs` (sidebar TOC) | +| Examples | `wiki/Examples.md` | `docs` (sidebar TOC) | +| Advanced Usage | `wiki/Advanced-Usage.md` | `docs` (sidebar TOC) | + +Each page includes: +- Auto-generated sidebar TOC from H2/H3 headings +- Scrollspy highlighting current section +- Previous/Next page navigation + +--- + +## Features + +### Day/Night Mode +- Toggle button in navbar (sun/moon icon) +- CSS custom properties for all colors +- Persisted in `localStorage` +- Falls back to `prefers-color-scheme` system preference +- Prevents FOUC with inline ` diff --git a/docs/_includes/navbar.html b/docs/_includes/navbar.html new file mode 100644 index 0000000..b13f7e4 --- /dev/null +++ b/docs/_includes/navbar.html @@ -0,0 +1,36 @@ + diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 0000000..62e399f --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,26 @@ + + + + {% include head.html %} + + + {% include navbar.html %} + +
+ {{ content }} +
+ + {% include footer.html %} + + + + + + + + + + + + + diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html new file mode 100644 index 0000000..f969878 --- /dev/null +++ b/docs/_layouts/docs.html @@ -0,0 +1,56 @@ + + + + {% include head.html %} + + + {% include navbar.html %} + +
+ + +
+ {{ content }} + + {% if page.prev_page or page.next_page %} + + {% endif %} +
+
+ + {% include footer.html %} + + + + + + + + + + + + + + diff --git a/docs/_layouts/home.html b/docs/_layouts/home.html new file mode 100644 index 0000000..85e4506 --- /dev/null +++ b/docs/_layouts/home.html @@ -0,0 +1,28 @@ + + + + {% include head.html %} + + + {% include navbar.html %} + +
+ {{ content }} +
+ + {% include footer.html %} + + + + + + + + + + + + + + + diff --git a/docs/_sass/_animations.scss b/docs/_sass/_animations.scss new file mode 100644 index 0000000..6c7e2c1 --- /dev/null +++ b/docs/_sass/_animations.scss @@ -0,0 +1,218 @@ +// ============================================== +// Animations — Timer Ninja +// ============================================== + +// Keyframes +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeInRight { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes scaleUp { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes typewriter { + from { width: 0; } + to { width: 100%; } +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +@keyframes ninja-appear { + 0% { + opacity: 0; + transform: scale(0.5) rotate(-10deg); + } + 60% { + opacity: 1; + transform: scale(1.05) rotate(2deg); + } + 100% { + opacity: 1; + transform: scale(1) rotate(0deg); + } +} + +// Scroll-triggered animation classes +// Elements start hidden and animate in when .is-animated is added +.animate-fade-in { + opacity: 0; + transition: opacity 0.6s ease; + + &.is-animated { + opacity: 1; + } +} + +.animate-fade-up { + opacity: 0; + transform: translateY(30px); + transition: opacity 0.6s ease, transform 0.6s ease; + + &.is-animated { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-left { + opacity: 0; + transform: translateX(-30px); + transition: opacity 0.6s ease, transform 0.6s ease; + + &.is-animated { + opacity: 1; + transform: translateX(0); + } +} + +.animate-fade-right { + opacity: 0; + transform: translateX(30px); + transition: opacity 0.6s ease, transform 0.6s ease; + + &.is-animated { + opacity: 1; + transform: translateX(0); + } +} + +.animate-scale-up { + opacity: 0; + transform: scale(0.9); + transition: opacity 0.5s ease, transform 0.5s ease; + + &.is-animated { + opacity: 1; + transform: scale(1); + } +} + +// Stagger delays for grid children +.stagger-children { + .animate-fade-up, + .animate-scale-up { + @for $i from 1 through 6 { + &:nth-child(#{$i}) { + transition-delay: #{($i - 1) * 0.1}s; + } + } + } +} + +// Parallax helper +.parallax { + will-change: transform; +} + +// Cursor blink for typing effect +.typing-cursor { + display: inline-block; + width: 2px; + height: 1.1em; + background: var(--color-primary); + animation: blink 0.8s step-end infinite; + margin-left: 2px; + vertical-align: text-bottom; +} + +// Page load entrance +body { + opacity: 0; + animation: pageEntrance 0.4s ease forwards; + animation-delay: 0.05s; +} + +@keyframes pageEntrance { + to { opacity: 1; } +} + +// Respect prefers-reduced-motion +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + body { + opacity: 1; + animation: none; + } + + .animate-fade-in, + .animate-fade-up, + .animate-fade-left, + .animate-fade-right, + .animate-scale-up { + opacity: 1; + transform: none; + } +} diff --git a/docs/_sass/_base.scss b/docs/_sass/_base.scss new file mode 100644 index 0000000..fcb2ef8 --- /dev/null +++ b/docs/_sass/_base.scss @@ -0,0 +1,258 @@ +// ============================================== +// Base Styles — Timer Ninja +// ============================================== + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: calc(var(--navbar-height) + 1rem); +} + +body { + font-family: var(--font-body); + font-size: $font-size-base; + line-height: 1.7; + color: var(--color-text); + background-color: var(--color-bg); + transition: background-color $transition-normal, color $transition-normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; +} + +// Typography +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.3; + color: var(--color-text); + margin-bottom: $space-md; +} + +h1 { font-size: 2.75rem; } +h2 { font-size: 2rem; } +h3 { font-size: 1.5rem; } +h4 { font-size: 1.25rem; } + +p { + margin-bottom: $space-md; + color: var(--color-text-muted); +} + +a { + color: var(--color-primary); + text-decoration: none; + transition: color $transition-fast; + + &:hover { + color: var(--color-primary-dark); + } +} + +// Code +code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--color-code-bg); + border: 1px solid var(--color-code-border); + border-radius: $radius-sm; + padding: 0.15em 0.4em; + color: var(--color-primary-dark); +} + +pre { + font-family: var(--font-mono); + font-size: 0.85rem; + line-height: 1.6; + background: var(--color-code-bg); + border: 1px solid var(--color-code-border); + border-radius: $radius-md; + padding: $space-lg; + overflow-x: auto; + margin-bottom: $space-lg; + + code { + background: none; + border: none; + padding: 0; + font-size: inherit; + color: inherit; + } +} + +// Container +.container { + max-width: $bp-xl; + margin: 0 auto; + padding: 0 $space-xl; + + @media (max-width: $bp-md) { + padding: 0 $space-lg; + } +} + +.container--narrow { + max-width: 900px; +} + +// Section +.section { + padding: $space-4xl 0; + + @media (max-width: $bp-md) { + padding: $space-3xl 0; + } +} + +.section__header { + text-align: center; + margin-bottom: $space-3xl; + + h2 { + font-size: 2.25rem; + margin-bottom: $space-sm; + } + + p { + font-size: 1.15rem; + max-width: 600px; + margin: 0 auto; + } +} + +// Badge +.badge { + display: inline-block; + font-size: 0.8rem; + font-weight: 600; + padding: 0.35em 0.9em; + border-radius: $radius-pill; + background: var(--color-primary-glow); + color: var(--color-primary); + letter-spacing: 0.03em; +} + +// Buttons +.btn { + display: inline-flex; + align-items: center; + gap: $space-sm; + font-family: var(--font-body); + font-size: 1rem; + font-weight: 600; + padding: 0.75rem 1.75rem; + border-radius: $radius-pill; + border: 2px solid transparent; + cursor: pointer; + transition: all $transition-normal; + text-decoration: none; + + &--primary { + background: var(--color-primary); + color: #fff; + border-color: var(--color-primary); + + &:hover { + background: var(--color-primary-dark); + border-color: var(--color-primary-dark); + color: #fff; + transform: translateY(-2px); + box-shadow: 0 6px 20px var(--color-primary-glow); + } + } + + &--outline { + background: transparent; + color: var(--color-text); + border-color: var(--color-border); + + &:hover { + border-color: var(--color-primary); + color: var(--color-primary); + transform: translateY(-2px); + } + } +} + +// Highlight/accent mark +.text-primary { + color: var(--color-primary); +} + +.text-gradient { + background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +// Selection +::selection { + background: rgba($primary, 0.25); + color: var(--color-text); +} + +// Scrollbar +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-surface); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); +} + +// HR +hr { + border: none; + height: 1px; + background: var(--color-border); + margin: $space-2xl 0; +} + +// Image +img { + max-width: 100%; + height: auto; +} + +// Table +table { + width: 100%; + border-collapse: collapse; + margin-bottom: $space-lg; + font-size: 0.9rem; + + th, td { + padding: $space-sm $space-md; + text-align: left; + border-bottom: 1px solid var(--color-border); + } + + th { + font-weight: 600; + color: var(--color-text); + background: var(--color-surface); + } + + td { + color: var(--color-text-muted); + } + + code { + font-size: 0.8rem; + } +} diff --git a/docs/_sass/_code.scss b/docs/_sass/_code.scss new file mode 100644 index 0000000..e86c334 --- /dev/null +++ b/docs/_sass/_code.scss @@ -0,0 +1,301 @@ +// ============================================== +// Code Panels & Comparison — Timer Ninja +// ============================================== + +.comparison { + background: var(--color-bg); +} + +.comparison__panels { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $space-xl; + margin-bottom: $space-2xl; + + @media (max-width: $bp-md) { + grid-template-columns: 1fr; + } +} + +.code-panel { + background: var(--color-code-bg); + border: 1px solid var(--color-code-border); + border-radius: $radius-lg; + overflow: hidden; + transition: all $transition-normal; + + &:hover { + box-shadow: 0 8px 30px var(--color-card-shadow); + } +} + +.code-panel__header { + padding: $space-md $space-lg; + font-size: 0.85rem; + font-weight: 600; + display: flex; + align-items: center; + gap: $space-sm; + border-bottom: 1px solid var(--color-code-border); +} + +.code-panel--before .code-panel__header { + background: rgba($accent-red, 0.06); + color: $accent-red; +} + +.code-panel--after .code-panel__header { + background: rgba($primary, 0.06); + color: $primary; +} + +.code-panel__body { + padding: $space-lg; + overflow-x: auto; + + pre { + margin: 0; + border: none; + background: none; + padding: 0; + font-size: 0.82rem; + line-height: 1.7; + } + + code { + background: none; + border: none; + padding: 0; + color: var(--color-text); + } +} + +.comparison__result { + text-align: center; + padding: $space-xl 0; + + .line-count { + display: inline-flex; + align-items: center; + gap: $space-md; + font-size: 1.3rem; + font-weight: 700; + } + + .lines-before { + color: $accent-red; + text-decoration: line-through; + opacity: 0.7; + } + + .lines-arrow { + color: var(--color-primary); + font-size: 1.5rem; + } + + .lines-after { + color: var(--color-primary); + } +} + +// Trace output section +.trace-section { + background: var(--color-surface); +} + +.trace-output { + background: var(--color-code-bg); + border: 1px solid var(--color-code-border); + border-radius: $radius-lg; + overflow: hidden; + max-width: 960px; + margin: 0 auto; +} + +.trace-output__header { + padding: $space-md $space-lg; + background: var(--color-surface-alt); + border-bottom: 1px solid var(--color-code-border); + display: flex; + align-items: center; + gap: $space-sm; + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-muted); + + .dot { + width: 10px; + height: 10px; + border-radius: 50%; + + &--red { background: #ff5f57; } + &--yellow { background: #ffbd2e; } + &--green { background: #28c840; } + } +} + +.trace-output__body { + padding: $space-lg; + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.8; + color: var(--color-text); + overflow-x: auto; + white-space: pre; + + .trace-line { + opacity: 0; + + &.is-visible { + opacity: 1; + } + } + + .trace-highlight { + color: var(--color-primary); + } + + .trace-threshold { + color: $accent-orange; + } + + .trace-args { + color: var(--color-text-muted); + } + + .trace-time { + color: var(--color-primary); + font-weight: 600; + } + + .trace-bracket { + color: var(--color-primary); + opacity: 0.6; + } +} + +// Quickstart section +.quickstart { + background: var(--color-bg); +} + +.quickstart__steps { + max-width: 780px; + margin: 0 auto; + position: relative; + + &::before { + content: ''; + position: absolute; + left: 28px; + top: 0; + bottom: 0; + width: 2px; + background: var(--color-border); + + @media (max-width: $bp-sm) { + left: 22px; + } + } +} + +.step-card { + display: flex; + gap: $space-xl; + padding: $space-xl; + margin-bottom: $space-lg; + background: var(--color-card-bg); + border: 1px solid var(--color-border); + border-radius: $radius-lg; + position: relative; + transition: all $transition-normal; + + &:hover { + border-color: rgba($primary, 0.3); + box-shadow: 0 4px 20px var(--color-card-shadow); + } + + @media (max-width: $bp-sm) { + flex-direction: column; + gap: $space-md; + padding: $space-lg; + } +} + +.step-card__number { + flex-shrink: 0; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--color-primary); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 1.2rem; + position: relative; + z-index: 1; + + @media (max-width: $bp-sm) { + width: 44px; + height: 44px; + font-size: 1rem; + } +} + +.step-card__content { + flex: 1; + min-width: 0; + + h3 { + font-size: 1.1rem; + margin-bottom: $space-sm; + } + + p { + font-size: 0.9rem; + margin-bottom: $space-md; + } + + pre { + font-size: 0.8rem; + margin-bottom: 0; + } +} + +// Tab switcher for Maven/Gradle +.tab-switcher { + display: inline-flex; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: $radius-pill; + padding: 3px; + margin-bottom: $space-md; +} + +.tab-switcher__btn { + padding: 0.35rem 1rem; + font-size: 0.8rem; + font-weight: 600; + border: none; + background: none; + border-radius: $radius-pill; + cursor: pointer; + color: var(--color-text-muted); + font-family: var(--font-body); + transition: all $transition-fast; + + &.is-active { + background: var(--color-primary); + color: #fff; + } +} + +.tab-content { + display: none; + + &.is-active { + display: block; + } +} diff --git a/docs/_sass/_dark-mode.scss b/docs/_sass/_dark-mode.scss new file mode 100644 index 0000000..29cec7d --- /dev/null +++ b/docs/_sass/_dark-mode.scss @@ -0,0 +1,72 @@ +// ============================================== +// Dark Mode — Timer Ninja +// ============================================== + +[data-theme="dark"] { + --color-bg: #{$dark-bg}; + --color-surface: #{$dark-surface}; + --color-surface-alt: #{$dark-surface-alt}; + --color-text: #{$dark-text}; + --color-text-muted: #{$dark-text-muted}; + --color-border: #{$dark-border}; + --color-code-bg: #{$dark-code-bg}; + --color-code-border: #{$dark-code-border}; + --color-card-bg: #{$dark-card-bg}; + --color-card-shadow: #{$dark-card-shadow}; + + .navbar { + background: rgba($dark-bg, 0.92); + } + + .hero { + background: linear-gradient(135deg, #0d1520 0%, #14202e 50%, #0d1520 100%); + } + + .hero__particles { + opacity: 0.5; + } + + .code-panel { + background: $dark-code-bg; + border-color: $dark-border; + } + + .code-panel--before .code-panel__header { + background: rgba($accent-red, 0.12); + color: lighten($accent-red, 15%); + } + + .code-panel--after .code-panel__header { + background: rgba($primary, 0.12); + color: $primary-light; + } + + .feature-card { + background: rgba($dark-surface, 0.7); + border-color: $dark-border; + } + + .feature-card:hover { + border-color: rgba($primary, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba($primary, 0.15); + } + + .trace-output { + background: $dark-code-bg; + border-color: $dark-border; + } + + .step-card { + background: $dark-surface; + border-color: $dark-border; + } + + // Syntax highlighting dark overrides + .highlight { + background: $dark-code-bg; + } + + img { + opacity: 0.92; + } +} diff --git a/docs/_sass/_docs.scss b/docs/_sass/_docs.scss new file mode 100644 index 0000000..e7550e5 --- /dev/null +++ b/docs/_sass/_docs.scss @@ -0,0 +1,173 @@ +// ============================================== +// Docs Layout — Timer Ninja +// ============================================== + +.docs { + display: flex; + min-height: calc(100vh - var(--navbar-height)); + padding-top: var(--navbar-height); +} + +.docs__sidebar { + width: 260px; + flex-shrink: 0; + position: sticky; + top: var(--navbar-height); + height: calc(100vh - var(--navbar-height)); + overflow-y: auto; + padding: $space-2xl $space-lg; + border-right: 1px solid var(--color-border); + background: var(--color-surface); + + @media (max-width: $bp-lg) { + display: none; + } +} + +.docs__sidebar-title { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-text-muted); + margin-bottom: $space-md; +} + +.docs__toc { + list-style: none; + + li { + margin-bottom: $space-xs; + } + + a { + display: block; + padding: $space-xs $space-md; + font-size: 0.85rem; + color: var(--color-text-muted); + border-radius: $radius-sm; + border-left: 2px solid transparent; + transition: all $transition-fast; + + &:hover { + color: var(--color-primary); + background: var(--color-primary-glow); + } + + &.is-active { + color: var(--color-primary); + border-left-color: var(--color-primary); + font-weight: 600; + } + } + + // Nested (h3 level) + ul { + list-style: none; + padding-left: $space-md; + + a { + font-size: 0.82rem; + } + } +} + +.docs__content { + flex: 1; + min-width: 0; + max-width: 860px; + padding: $space-2xl $space-3xl; + + @media (max-width: $bp-lg) { + padding: $space-xl $space-lg; + max-width: 100%; + } +} + +.docs__content h1 { + font-size: 2.25rem; + margin-bottom: $space-sm; + padding-bottom: $space-md; + border-bottom: 2px solid var(--color-border); +} + +.docs__content h2 { + font-size: 1.65rem; + margin-top: $space-3xl; + margin-bottom: $space-lg; + padding-bottom: $space-sm; + border-bottom: 1px solid var(--color-border); +} + +.docs__content h3 { + font-size: 1.25rem; + margin-top: $space-2xl; + margin-bottom: $space-md; +} + +.docs__content h4 { + font-size: 1.05rem; + margin-top: $space-xl; +} + +.docs__content blockquote { + border-left: 4px solid var(--color-primary); + padding: $space-md $space-lg; + margin: $space-lg 0; + background: var(--color-surface); + border-radius: 0 $radius-md $radius-md 0; + color: var(--color-text-muted); + + p:last-child { + margin-bottom: 0; + } +} + +.docs__content ul, .docs__content ol { + padding-left: $space-xl; + margin-bottom: $space-lg; + + li { + margin-bottom: $space-xs; + color: var(--color-text-muted); + } +} + +// Page nav +.docs__page-nav { + display: flex; + justify-content: space-between; + gap: $space-lg; + margin-top: $space-3xl; + padding-top: $space-xl; + border-top: 1px solid var(--color-border); +} + +.docs__page-nav a { + display: flex; + flex-direction: column; + gap: $space-xs; + padding: $space-md $space-lg; + border: 1px solid var(--color-border); + border-radius: $radius-md; + transition: all $transition-fast; + max-width: 45%; + + &:hover { + border-color: var(--color-primary); + background: var(--color-surface); + } + + .nav-label { + font-size: 0.75rem; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .nav-title { + font-weight: 600; + color: var(--color-text); + font-size: 0.95rem; + } +} diff --git a/docs/_sass/_features.scss b/docs/_sass/_features.scss new file mode 100644 index 0000000..33f07a0 --- /dev/null +++ b/docs/_sass/_features.scss @@ -0,0 +1,81 @@ +// ============================================== +// Features — Timer Ninja +// ============================================== + +.features { + background: var(--color-surface); +} + +.features__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $space-xl; + + @media (max-width: $bp-lg) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: $bp-sm) { + grid-template-columns: 1fr; + } +} + +.feature-card { + background: var(--color-card-bg); + border: 1px solid var(--color-border); + border-radius: $radius-lg; + padding: $space-2xl; + transition: all $transition-normal; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--color-primary), var(--color-primary-light)); + transform: scaleX(0); + transition: transform $transition-normal; + transform-origin: left; + } + + &:hover { + transform: translateY(-6px); + box-shadow: 0 12px 40px var(--color-card-shadow); + border-color: rgba($primary, 0.3); + + &::before { + transform: scaleX(1); + } + } +} + +.feature-card__icon { + width: 52px; + height: 52px; + border-radius: $radius-md; + background: var(--color-primary-glow); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: $space-lg; + font-size: 1.5rem; + color: var(--color-primary); +} + +.feature-card__title { + font-size: 1.15rem; + font-weight: 700; + margin-bottom: $space-sm; + color: var(--color-text); +} + +.feature-card__desc { + font-size: 0.92rem; + color: var(--color-text-muted); + line-height: 1.7; + margin-bottom: 0; +} diff --git a/docs/_sass/_footer.scss b/docs/_sass/_footer.scss new file mode 100644 index 0000000..835ad6c --- /dev/null +++ b/docs/_sass/_footer.scss @@ -0,0 +1,113 @@ +// ============================================== +// Footer — Timer Ninja +// ============================================== + +.footer { + background: var(--color-surface); + border-top: 1px solid var(--color-border); + padding: $space-3xl 0 $space-xl; +} + +.footer__inner { + max-width: $bp-xl; + margin: 0 auto; + padding: 0 $space-xl; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.footer__mascot { + width: 64px; + height: 64px; + margin-bottom: $space-lg; + opacity: 0.8; + transition: opacity $transition-normal; + + &:hover { + opacity: 1; + } + + img { + width: 100%; + height: 100%; + object-fit: contain; + } +} + +.footer__tagline { + font-size: 0.9rem; + color: var(--color-text-muted); + margin-bottom: $space-lg; +} + +.footer__links { + display: flex; + align-items: center; + gap: $space-xl; + margin-bottom: $space-xl; + list-style: none; + flex-wrap: wrap; + justify-content: center; + + a { + font-size: 0.85rem; + color: var(--color-text-muted); + transition: color $transition-fast; + + &:hover { + color: var(--color-primary); + } + } +} + +.footer__copy { + font-size: 0.8rem; + color: var(--color-text-muted); + opacity: 0.7; +} + +// Back to top button +.back-to-top { + position: fixed; + bottom: $space-xl; + right: $space-xl; + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transform: translateY(12px); + transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease, + background-color $transition-fast, color $transition-fast, box-shadow $transition-fast; + z-index: 90; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + + &.is-visible { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + + &:hover { + background: var(--color-primary); + color: #fff; + border-color: var(--color-primary); + box-shadow: 0 4px 16px rgba(70, 191, 198, 0.35); + } + + @media (max-width: $bp-sm) { + bottom: $space-lg; + right: $space-lg; + width: 40px; + height: 40px; + } +} diff --git a/docs/_sass/_hero.scss b/docs/_sass/_hero.scss new file mode 100644 index 0000000..76234cb --- /dev/null +++ b/docs/_sass/_hero.scss @@ -0,0 +1,122 @@ +// ============================================== +// Hero — Timer Ninja +// ============================================== + +.hero { + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + overflow: hidden; + background: linear-gradient(135deg, #f0fafa 0%, #e0f4f4 30%, #f5f9fa 60%, #e8f6f6 100%); + padding-top: var(--navbar-height); +} + +.hero__particles { + position: absolute; + inset: 0; + z-index: 0; +} + +.hero__content { + position: relative; + z-index: 1; + max-width: 800px; + padding: $space-2xl; +} + +.hero__mascot { + width: 140px; + height: 140px; + margin: 0 auto $space-xl; + animation: float 4s ease-in-out infinite; + filter: drop-shadow(0 12px 24px rgba(70, 191, 198, 0.2)); + + img { + width: 100%; + height: 100%; + object-fit: contain; + } +} + +.hero__badge { + margin-bottom: $space-lg; +} + +.hero__title { + font-size: 3.5rem; + font-weight: 800; + line-height: 1.15; + margin-bottom: $space-lg; + color: var(--color-text); + + @media (max-width: $bp-md) { + font-size: 2.5rem; + } + + @media (max-width: $bp-sm) { + font-size: 2rem; + } +} + +.hero__subtitle { + font-size: 1.25rem; + color: var(--color-text-muted); + max-width: 550px; + margin: 0 auto $space-2xl; + line-height: 1.7; + + @media (max-width: $bp-sm) { + font-size: 1.05rem; + } +} + +.hero__actions { + display: flex; + align-items: center; + justify-content: center; + gap: $space-md; + margin-bottom: $space-2xl; + flex-wrap: wrap; +} + +.hero__version { + display: inline-flex; + align-items: center; + gap: $space-sm; + font-family: var(--font-mono); + font-size: 0.85rem; + color: var(--color-text-muted); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: $radius-pill; + padding: 0.5rem 1rem; + + .version-label { + color: var(--color-primary); + font-weight: 600; + } +} + +.hero__scroll-hint { + position: absolute; + bottom: $space-2xl; + left: 50%; + transform: translateX(-50%); + color: var(--color-text-muted); + animation: bounce 2s ease-in-out infinite; + opacity: 0.6; + font-size: 1.5rem; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-14px); } +} + +@keyframes bounce { + 0%, 100% { transform: translateX(-50%) translateY(0); } + 50% { transform: translateX(-50%) translateY(8px); } +} diff --git a/docs/_sass/_navbar.scss b/docs/_sass/_navbar.scss new file mode 100644 index 0000000..6944d8e --- /dev/null +++ b/docs/_sass/_navbar.scss @@ -0,0 +1,192 @@ +// ============================================== +// Navbar — Timer Ninja +// ============================================== + +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--navbar-height); + z-index: $z-navbar; + background: rgba($light-bg, 0.88); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--color-border); + transition: background $transition-normal, border-color $transition-normal; +} + +.navbar__inner { + max-width: $bp-xl; + margin: 0 auto; + padding: 0 $space-xl; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} + +.navbar__brand { + display: flex; + align-items: center; + gap: $space-sm; + text-decoration: none; + font-weight: 700; + font-size: 1.2rem; + color: var(--color-text); + transition: color $transition-fast; + + img { + width: 34px; + height: 34px; + border-radius: 50%; + } + + &:hover { + color: var(--color-primary); + } +} + +.navbar__links { + display: flex; + align-items: center; + gap: $space-lg; + list-style: none; + + @media (max-width: $bp-md) { + display: none; + + &.is-open { + display: flex; + flex-direction: column; + position: fixed; + top: var(--navbar-height); + left: 0; + right: 0; + bottom: 0; + background: var(--color-bg); + padding: $space-2xl; + gap: $space-xl; + z-index: $z-navbar; + animation: fadeIn 0.2s ease; + } + } +} + +.navbar__link { + font-size: 0.92rem; + font-weight: 500; + color: var(--color-text-muted); + text-decoration: none; + transition: color $transition-fast; + position: relative; + + &::after { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + right: 0; + height: 2px; + background: var(--color-primary); + border-radius: 1px; + transform: scaleX(0); + transition: transform $transition-normal; + } + + &:hover, + &.is-active { + color: var(--color-primary); + + &::after { + transform: scaleX(1); + } + } +} + +.navbar__actions { + display: flex; + align-items: center; + gap: $space-md; +} + +// Theme toggle +.theme-toggle { + background: none; + border: 2px solid var(--color-border); + border-radius: 50%; + width: 38px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--color-text-muted); + transition: all $transition-normal; + font-size: 1.1rem; + + &:hover { + border-color: var(--color-primary); + color: var(--color-primary); + transform: rotate(20deg); + } + + .icon-sun, + .icon-moon { + transition: opacity $transition-fast, transform $transition-normal; + } + + .icon-moon { display: none; } +} + +[data-theme="dark"] .theme-toggle { + .icon-sun { display: none; } + .icon-moon { display: block; } +} + +// GitHub link +.navbar__github { + color: var(--color-text-muted); + transition: color $transition-fast; + + &:hover { + color: var(--color-primary); + } + + svg { + width: 22px; + height: 22px; + fill: currentColor; + } +} + +// Hamburger +.navbar__hamburger { + display: none; + background: none; + border: none; + cursor: pointer; + padding: $space-sm; + color: var(--color-text); + + @media (max-width: $bp-md) { + display: flex; + flex-direction: column; + gap: 5px; + } + + span { + display: block; + width: 22px; + height: 2px; + background: currentColor; + border-radius: 1px; + transition: all $transition-fast; + } + + &.is-open { + span:nth-child(1) { transform: rotate(45deg) translate(5px, 5px); } + span:nth-child(2) { opacity: 0; } + span:nth-child(3) { transform: rotate(-45deg) translate(5px, -5px); } + } +} diff --git a/docs/_sass/_prism-overrides.scss b/docs/_sass/_prism-overrides.scss new file mode 100644 index 0000000..38a2299 --- /dev/null +++ b/docs/_sass/_prism-overrides.scss @@ -0,0 +1,85 @@ +// ============================================== +// Prism.js Overrides — Timer Ninja +// Make Prism's styling blend with our design system +// ============================================== + +// Override Prism's default backgrounds to use our tokens +pre[class*="language-"], +code[class*="language-"] { + font-family: var(--font-mono) !important; + font-size: 0.84rem !important; + line-height: 1.65 !important; +} + +pre[class*="language-"] { + background: var(--color-code-bg) !important; + border: 1px solid var(--color-code-border) !important; + border-radius: $radius-md !important; + padding: $space-lg !important; + margin-bottom: $space-lg !important; +} + +// Prism toolbar (copy button) +div.code-toolbar > .toolbar { + opacity: 0; + transition: opacity $transition-normal; +} + +div.code-toolbar:hover > .toolbar { + opacity: 1; +} + +div.code-toolbar > .toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar > .toolbar-item > span { + font-family: var(--font-body) !important; + font-size: 0.72rem !important; + font-weight: 600 !important; + padding: 4px 12px !important; + border-radius: $radius-pill !important; + background: var(--color-primary) !important; + color: #fff !important; + box-shadow: 0 2px 8px rgba(0,0,0,0.12) !important; + border: none !important; + cursor: pointer; + transition: all $transition-fast !important; + + &:hover { + background: var(--color-primary-dark) !important; + transform: translateY(-1px); + } +} + +// Rouge/Jekyll highlight wrapper +.highlighter-rouge { + margin-bottom: $space-lg; +} + +.highlight { + pre { + margin-bottom: 0; + } +} + +// Inline code should not be affected by Prism +:not(pre) > code[class*="language-"] { + background: var(--color-code-bg) !important; + border: 1px solid var(--color-code-border) !important; + border-radius: $radius-sm !important; + padding: 0.15em 0.4em !important; + color: var(--color-primary-dark) !important; + font-size: 0.875em !important; +} + +// Dark mode token overrides for Prism +[data-theme="dark"] { + pre[class*="language-"] { + background: var(--color-code-bg) !important; + border-color: var(--color-code-border) !important; + } + + :not(pre) > code[class*="language-"] { + background: var(--color-code-bg) !important; + border-color: var(--color-code-border) !important; + color: var(--color-primary-light) !important; + } +} diff --git a/docs/_sass/_variables.scss b/docs/_sass/_variables.scss new file mode 100644 index 0000000..0a5b80c --- /dev/null +++ b/docs/_sass/_variables.scss @@ -0,0 +1,109 @@ +// ============================================== +// Design Tokens — Timer Ninja +// ============================================== + +// Brand colors +$primary: #46bfc6; +$primary-dark: #3aa3a9; +$primary-light: #6dd5db; +$primary-glow: rgba(70, 191, 198, 0.25); + +// Light mode +$light-bg: #ffffff; +$light-surface: #f5fafa; +$light-surface-alt: #eaf5f5; +$light-text: #1a2b3c; +$light-text-muted: #5a6b7c; +$light-border: #d4e8e8; +$light-code-bg: #f0f6f6; +$light-code-border: #d4e8e8; +$light-card-bg: #ffffff; +$light-card-shadow: rgba(70, 191, 198, 0.08); + +// Dark mode +$dark-bg: #0d1520; +$dark-surface: #14202e; +$dark-surface-alt: #1a2b3c; +$dark-text: #e8f0f2; +$dark-text-muted: #8fa3b2; +$dark-border: #243444; +$dark-code-bg: #111d2b; +$dark-code-border: #243444; +$dark-card-bg: #14202e; +$dark-card-shadow: rgba(0, 0, 0, 0.3); + +// Accent / semantic +$accent-red: #e74c5a; +$accent-green: #2ecc71; +$accent-yellow: #f0c040; +$accent-orange: #f39c12; + +// Typography +$font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +$font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace; +$font-size-base: 1rem; + +// Spacing +$space-xs: 0.25rem; +$space-sm: 0.5rem; +$space-md: 1rem; +$space-lg: 1.5rem; +$space-xl: 2rem; +$space-2xl: 3rem; +$space-3xl: 4rem; +$space-4xl: 6rem; + +// Breakpoints +$bp-sm: 576px; +$bp-md: 768px; +$bp-lg: 1024px; +$bp-xl: 1280px; + +// Border radius +$radius-sm: 6px; +$radius-md: 10px; +$radius-lg: 16px; +$radius-xl: 24px; +$radius-pill: 50px; + +// Transition +$transition-fast: 0.15s ease; +$transition-normal: 0.3s ease; +$transition-slow: 0.5s ease; + +// Z-index +$z-navbar: 1000; +$z-modal: 2000; +$z-tooltip: 3000; + +// ===== CSS Custom Properties (for theming) ===== +:root { + --color-primary: #{$primary}; + --color-primary-dark: #{$primary-dark}; + --color-primary-light: #{$primary-light}; + --color-primary-glow: #{$primary-glow}; + + --color-bg: #{$light-bg}; + --color-surface: #{$light-surface}; + --color-surface-alt: #{$light-surface-alt}; + --color-text: #{$light-text}; + --color-text-muted: #{$light-text-muted}; + --color-border: #{$light-border}; + --color-code-bg: #{$light-code-bg}; + --color-code-border: #{$light-code-border}; + --color-card-bg: #{$light-card-bg}; + --color-card-shadow: #{$light-card-shadow}; + + --color-red: #{$accent-red}; + --color-green: #{$accent-green}; + --color-yellow: #{$accent-yellow}; + + --font-body: #{$font-body}; + --font-mono: #{$font-mono}; + + --radius-sm: #{$radius-sm}; + --radius-md: #{$radius-md}; + --radius-lg: #{$radius-lg}; + + --navbar-height: 64px; +} diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md new file mode 100644 index 0000000..2e3fbcc --- /dev/null +++ b/docs/advanced-usage.md @@ -0,0 +1,387 @@ +--- +layout: docs +title: Advanced Usage +description: "Advanced features, optimization techniques, and best practices for Timer Ninja." +prev_page: + title: Examples + url: /examples/ +--- + +# Advanced Usage + +This guide covers advanced features and optimization techniques for Timer Ninja. + +--- + +## Nested Tracking Deep Dive + +Timer Ninja automatically detects and preserves nested method calls that are also annotated with `@TimerNinjaTracker`. This provides a complete view of the execution stack. + +### Multi-Level Nesting Example + +```java +@Service +public class OrderProcessingService { + + @TimerNinjaTracker + public void processOrder(Order order) { + validateOrder(order); + processPayment(order); + shipOrder(order); + } + + @TimerNinjaTracker + private void validateOrder(Order order) { + validateCustomer(order.getCustomerId()); + validateItems(order.getItems()); + } + + @TimerNinjaTracker + private void validateCustomer(Long customerId) { + Customer customer = customerService.findById(customerId); + } + + @TimerNinjaTracker(threshold = 100) + private void validateItems(List items) { + items.forEach(this::validateItem); + } + + @TimerNinjaTracker(threshold = 50) + private void validateItem(OrderItem item) { + // Item validation + } +} +``` + +**Output:** +``` +{===== Start of trace context id: abc123... =====} +public void processOrder(Order order) - 1850 ms + |-- private void validateOrder(Order order) - 450 ms + | |-- private void validateCustomer(Long customerId) - 320 ms + | |-- private void validateItems(List items) - 110 ms + | |-- private void validateItem(OrderItem item) - 52 ms ¤ [Threshold Exceed !!: 50 ms] + |-- public void processPayment(Order order) - 1200 ms + |-- public void shipOrder(Order order) - 200 ms +{====== End of trace context id: abc123... ======} +``` + +### Key Points + +1. **Automatic Hierarchy** — Timer Ninja automatically builds the call tree +2. **Independent Thresholds** — Each method can have its own threshold +3. **Context Sharing** — All methods in a call chain share the same trace context ID + +--- + +## Advanced Threshold Strategies + +### Dynamic Thresholds Based on Input + +```java +@Service +public class QueryService { + + @TimerNinjaTracker(includeArgs = true) + public void executeQuery(String query, int expectedRows) { + int threshold = calculateThreshold(expectedRows); + + BlockTrackerConfig config = new BlockTrackerConfig() + .setThreshold(threshold) + .setTimeUnit(ChronoUnit.MILLIS); + + TimerNinjaBlock.measure("query execution", config, () -> { + database.execute(query); + }); + } + + private int calculateThreshold(int expectedRows) { + return Math.min(100 + (expectedRows / 10), 1000); + } +} +``` + +### Threshold Tiers + +```java +@Service +public class TieredTrackingService { + + @TimerNinjaTracker(threshold = 50) // Fast operations + public void cacheLookup(String key) { } + + @TimerNinjaTracker(threshold = 200) // Standard operations + public void databaseQuery(String query) { } + + @TimerNinjaTracker(threshold = 1000) // Slow operations + public void externalApiCall(String endpoint) { } + + @TimerNinjaTracker(threshold = 5000) // Very slow operations + public void batchProcess(String batchId) { } +} +``` + +--- + +## Block Tracking Patterns + +### Pattern 1: Phased Processing + +```java +@Service +public class DataPipelineService { + + @TimerNinjaTracker + public void runPipeline(String dataId) { + RawData raw = TimerNinjaBlock.measure("extract", () -> { + return extractor.extract(dataId); + }); + + ProcessedData processed = TimerNinjaBlock.measure("transform", () -> { + return transformer.transform(raw); + }); + + TimerNinjaBlock.measure("load", () -> { + loader.load(processed); + }); + + TimerNinjaBlock.measure("cleanup", () -> { + cleanupService.cleanup(dataId); + }); + } +} +``` + +### Pattern 2: Conditional Tracking + +```java +@Service +public class ConditionalTrackingService { + + @TimerNinjaTracker + public void processWithTracking(boolean enableDetailedTracking, Data data) { + processMain(data); + + if (enableDetailedTracking) { + TimerNinjaBlock.measure("detailed validation", () -> { + validateDetailed(data); + }); + + TimerNinjaBlock.measure("detailed transformation", () -> { + transformDetailed(data); + }); + } + } +} +``` + +### Pattern 3: Retry Logic Tracking + +```java +@Service +public class RetryTrackingService { + + @TimerNinjaTracker + public Result executeWithRetry(String operation, Data data) { + int maxRetries = 3; + int attempt = 0; + + while (attempt < maxRetries) { + attempt++; + try { + Result result = TimerNinjaBlock.measure( + String.format("attempt %d", attempt), + () -> executeOperation(operation, data) + ); + return result; + } catch (Exception e) { + if (attempt == maxRetries) { + throw new RuntimeException("Failed after " + maxRetries + " attempts", e); + } + TimerNinjaBlock.measure("retry delay", () -> { + Thread.sleep(calculateBackoff(attempt)); + }); + } + } + throw new IllegalStateException("Should not reach here"); + } +} +``` + +--- + +## Performance Optimization + +### Minimizing Overhead + +#### 1. Selective Tracking + +```java +// ❌ Bad - tracking everything +@TimerNinjaTracker +public String getUserName(Long userId) { + return userRepository.findById(userId).getName(); +} + +// ✅ Good - tracking the meaningful operation +@Repository +public class UserRepository { + @TimerNinjaTracker + public User findById(Long userId) { + // Database query + } +} +``` + +#### 2. Use Thresholds Effectively + +```java +// ✅ Appropriate threshold +@TimerNinjaTracker(threshold = 100) +public void meaningfulOperation() { + // Operation that should be reasonably fast +} +``` + +#### 3. Avoid Circular Dependencies in toString() + +```java +// ❌ Bad - circular reference +public class User { + private List orders; + + @Override + public String toString() { + return "User{orders=" + orders + "}"; // Orders contain Users! + } +} + +// ✅ Good - selective toString() +public class User { + private List orders; + + @Override + public String toString() { + return String.format("User{id=%d, ordersCount=%d}", id, orders.size()); + } +} +``` + +--- + +## Integration with Spring Boot + +```java +@Configuration +public class TimerNinjaConfig { + + @Bean + public CommandLineRunner setupTimerNinja() { + return args -> { + // Enable System.out for development + if (isDevelopmentEnvironment()) { + io.github.thanglequoc.timerninja.TimerNinjaConfiguration + .getInstance() + .toggleSystemOutLog(true); + } + }; + } +} + +@RestController +@RequestMapping("/api") +public class ApiController { + + @TimerNinjaTracker + @GetMapping("/data/{id}") + public ResponseEntity getData(@PathVariable Long id) { + return ResponseEntity.ok(dataService.findById(id)); + } +} +``` + +--- + +## Custom Logging Strategies + +### Custom Log Formats + +Timer Ninja uses SLF4J, so you can customize the format through your logging configuration: + +**logback.xml** +```xml + + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + +``` + +--- + +## Troubleshooting Complex Scenarios + +### Missing Nested Traces + +1. Verify annotations are present on nested methods +2. Check if methods are private/internal — all access levels are tracked if annotated +3. Verify AspectJ weaving is working: `aspect 'io.github.thanglequoc:timer-ninja:1.3.0'` + +### Large Trace Outputs + +1. Use thresholds to filter noise: `@TimerNinjaTracker(threshold = 200)` +2. Disable argument logging by default, enable selectively +3. Use block tracking for phases instead of individual method tracking + +### Thread Safety + +Timer Ninja is thread-safe by design. Each thread maintains its own trace context via `ThreadLocal`: + +```java +@TimerNinjaTracker +public void parallelProcessing() { + ExecutorService executor = Executors.newFixedThreadPool(4); + + // Each task gets its own trace context + for (int i = 0; i < 10; i++) { + final int taskId = i; + executor.submit(() -> { + processTask(taskId); // Independent trace per thread + }); + } + executor.shutdown(); +} +``` + +--- + +## Best Practices Summary + +### Do ✅ + +1. Track entry points (controllers, main methods, public services) +2. Use appropriate thresholds based on expected performance +3. Enable argument logging for debugging critical operations +4. Combine annotation and block tracking for comprehensive monitoring +5. Track external operations (API calls, database queries, file I/O) +6. Monitor constructor chains for slow initialization + +### Don't ❌ + +1. Track every method — focus on meaningful operations +2. Use very low thresholds — creates noise +3. Log sensitive data with `includeArgs` +4. Create circular `toString()` references +5. Track simple getters/setters +6. Forget to configure AspectJ weaving diff --git a/docs/assets/css/main.scss b/docs/assets/css/main.scss new file mode 100644 index 0000000..224b3fd --- /dev/null +++ b/docs/assets/css/main.scss @@ -0,0 +1,14 @@ +--- +# Main SCSS entry — imports all partials +--- +@import "variables"; +@import "base"; +@import "navbar"; +@import "hero"; +@import "features"; +@import "code"; +@import "docs"; +@import "animations"; +@import "footer"; +@import "prism-overrides"; +@import "dark-mode"; diff --git a/docs/assets/images/mascot.png b/docs/assets/images/mascot.png new file mode 100644 index 0000000..a4030b3 Binary files /dev/null and b/docs/assets/images/mascot.png differ diff --git a/docs/assets/js/animations.js b/docs/assets/js/animations.js new file mode 100644 index 0000000..cd2702a --- /dev/null +++ b/docs/assets/js/animations.js @@ -0,0 +1,138 @@ +// ============================================== +// Scroll Animations — Timer Ninja +// IntersectionObserver-based scroll reveals +// ============================================== + +(function () { + 'use strict'; + + var ANIMATED_CLASS = 'is-animated'; + var SELECTORS = [ + '.animate-fade-in', + '.animate-fade-up', + '.animate-fade-left', + '.animate-fade-right', + '.animate-scale-up' + ].join(','); + + function initAnimations() { + var elements = document.querySelectorAll(SELECTORS); + if (!elements.length) return; + + // Feature check + if (!('IntersectionObserver' in window)) { + elements.forEach(function (el) { + el.classList.add(ANIMATED_CLASS); + }); + return; + } + + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add(ANIMATED_CLASS); + observer.unobserve(entry.target); + } + }); + }, { + root: null, + rootMargin: '0px 0px -60px 0px', + threshold: 0.1 + }); + + elements.forEach(function (el) { + observer.observe(el); + }); + } + + // Parallax effect for hero + scroll hint fade-out + function initParallax() { + var hero = document.querySelector('.hero'); + var mascot = document.querySelector('.hero__mascot'); + var scrollHint = document.querySelector('.hero__scroll-hint'); + if (!hero || !mascot) return; + + window.addEventListener('scroll', function () { + var scrollY = window.scrollY; + var heroHeight = hero.offsetHeight; + + if (scrollY < heroHeight) { + var progress = scrollY / heroHeight; + mascot.style.transform = 'translateY(' + (scrollY * 0.15) + 'px)'; + hero.style.backgroundPositionY = (progress * 30) + '%'; + } + + // Fade out scroll hint after 120px + if (scrollHint) { + var hintOpacity = Math.max(0, 0.6 - scrollY / 200); + scrollHint.style.opacity = hintOpacity; + scrollHint.style.pointerEvents = hintOpacity < 0.1 ? 'none' : ''; + } + }, { passive: true }); + } + + // Trace output line-by-line reveal + function initTraceAnimation() { + var traceBody = document.getElementById('traceBody'); + if (!traceBody) return; + + var lines = traceBody.querySelectorAll('.trace-line'); + if (!lines.length) return; + + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + lines.forEach(function (line, index) { + setTimeout(function () { + line.classList.add('is-visible'); + }, index * 120); + }); + observer.unobserve(entry.target); + } + }); + }, { + threshold: 0.3 + }); + + observer.observe(traceBody); + } + + // Tab switcher + function initTabs() { + document.querySelectorAll('.tab-switcher').forEach(function (switcher) { + var buttons = switcher.querySelectorAll('.tab-switcher__btn'); + buttons.forEach(function (btn) { + btn.addEventListener('click', function () { + var tabId = btn.getAttribute('data-tab'); + var parent = switcher.parentElement; + + // Update buttons + buttons.forEach(function (b) { b.classList.remove('is-active'); }); + btn.classList.add('is-active'); + + // Update content + parent.querySelectorAll('.tab-content').forEach(function (tc) { + tc.classList.remove('is-active'); + }); + var target = document.getElementById(tabId); + if (target) target.classList.add('is-active'); + }); + }); + }); + } + + // Init all + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { + initAnimations(); + initParallax(); + initTraceAnimation(); + initTabs(); + }); + } else { + initAnimations(); + initParallax(); + initTraceAnimation(); + initTabs(); + } +})(); diff --git a/docs/assets/js/docs-toc.js b/docs/assets/js/docs-toc.js new file mode 100644 index 0000000..c28cf96 --- /dev/null +++ b/docs/assets/js/docs-toc.js @@ -0,0 +1,78 @@ +// ============================================== +// Docs TOC — Timer Ninja +// Auto-generate sidebar TOC from page headings + scrollspy +// ============================================== + +(function () { + 'use strict'; + + var tocContainer = document.getElementById('docsToc'); + var content = document.querySelector('.docs__content'); + if (!tocContainer || !content) return; + + // Gather h2 and h3 headings + var headings = content.querySelectorAll('h2, h3'); + if (!headings.length) return; + + var tocHTML = ''; + var currentH2 = null; + + headings.forEach(function (heading) { + // Ensure heading has an ID + if (!heading.id) { + heading.id = heading.textContent + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim(); + } + + var text = heading.textContent; + var id = heading.id; + + if (heading.tagName === 'H2') { + if (currentH2) { + tocHTML += ''; + } + tocHTML += '
  • ' + text + '
      '; + currentH2 = heading; + } else if (heading.tagName === 'H3') { + tocHTML += '
    • ' + text + '
    • '; + } + }); + + if (currentH2) { + tocHTML += '
  • '; + } + + tocContainer.innerHTML = tocHTML; + + // Scrollspy + var tocLinks = tocContainer.querySelectorAll('a'); + var headingElements = Array.from(headings); + + function updateActiveLink() { + var scrollPos = window.scrollY + 100; + var activeId = ''; + + for (var i = headingElements.length - 1; i >= 0; i--) { + if (headingElements[i].offsetTop <= scrollPos) { + activeId = headingElements[i].id; + break; + } + } + + tocLinks.forEach(function (link) { + var href = link.getAttribute('href').substring(1); + if (href === activeId) { + link.classList.add('is-active'); + } else { + link.classList.remove('is-active'); + } + }); + } + + window.addEventListener('scroll', updateActiveLink, { passive: true }); + updateActiveLink(); +})(); diff --git a/docs/assets/js/particles.js b/docs/assets/js/particles.js new file mode 100644 index 0000000..50429e5 --- /dev/null +++ b/docs/assets/js/particles.js @@ -0,0 +1,118 @@ +// ============================================== +// Particles — Timer Ninja +// Lightweight canvas particle effect for hero +// ============================================== + +(function () { + 'use strict'; + + var canvas = document.getElementById('heroParticles'); + if (!canvas) return; + + var ctx = canvas.getContext('2d'); + var particles = []; + var PARTICLE_COUNT = 60; + var CONNECTION_DISTANCE = 120; + var animationId; + + function isDark() { + return document.documentElement.getAttribute('data-theme') === 'dark'; + } + + function getColor(alpha) { + return isDark() + ? 'rgba(109, 213, 219, ' + alpha + ')' + : 'rgba(70, 191, 198, ' + alpha + ')'; + } + + function resize() { + var hero = canvas.parentElement; + canvas.width = hero.offsetWidth; + canvas.height = hero.offsetHeight; + } + + function Particle() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.vx = (Math.random() - 0.5) * 0.4; + this.vy = (Math.random() - 0.5) * 0.4; + this.radius = Math.random() * 2.5 + 0.8; + this.alpha = Math.random() * 0.4 + 0.15; + } + + function createParticles() { + particles = []; + for (var i = 0; i < PARTICLE_COUNT; i++) { + particles.push(new Particle()); + } + } + + function drawParticles() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw connections + for (var i = 0; i < particles.length; i++) { + for (var j = i + 1; j < particles.length; j++) { + var dx = particles[i].x - particles[j].x; + var dy = particles[i].y - particles[j].y; + var dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < CONNECTION_DISTANCE) { + var alpha = (1 - dist / CONNECTION_DISTANCE) * 0.15; + ctx.beginPath(); + ctx.strokeStyle = getColor(alpha); + ctx.lineWidth = 0.6; + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.stroke(); + } + } + } + + // Draw particles + for (var k = 0; k < particles.length; k++) { + var p = particles[k]; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); + ctx.fillStyle = getColor(p.alpha); + ctx.fill(); + + // Update position + p.x += p.vx; + p.y += p.vy; + + // Bounce off edges + if (p.x < 0 || p.x > canvas.width) p.vx *= -1; + if (p.y < 0 || p.y > canvas.height) p.vy *= -1; + } + + animationId = requestAnimationFrame(drawParticles); + } + + // Pause when not visible + var heroSection = canvas.parentElement; + + function checkVisibility() { + var rect = heroSection.getBoundingClientRect(); + var isVisible = rect.bottom > 0 && rect.top < window.innerHeight; + + if (isVisible && !animationId) { + animationId = requestAnimationFrame(drawParticles); + } else if (!isVisible && animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + } + + // Init + resize(); + createParticles(); + drawParticles(); + + window.addEventListener('resize', function () { + resize(); + createParticles(); + }); + + window.addEventListener('scroll', checkVisibility, { passive: true }); +})(); diff --git a/docs/assets/js/prism-init.js b/docs/assets/js/prism-init.js new file mode 100644 index 0000000..0c8c175 --- /dev/null +++ b/docs/assets/js/prism-init.js @@ -0,0 +1,95 @@ +// ============================================== +// Prism.js Init — Timer Ninja +// Bridges Jekyll/Rouge code blocks to Prism.js +// and syncs Prism theme with day/night mode +// ============================================== + +(function () { + 'use strict'; + + // Rouge (Jekyll's highlighter) adds classes like `language-java` or + // `highlight` with nested `` blocks. Prism expects + // `` inside a `
    `.
    +  // This script normalizes Rouge output for Prism compatibility.
    +
    +  function bridgeRougeToprism() {
    +    // Handle Rouge-generated blocks: 
    + document.querySelectorAll('div[class*="language-"]').forEach(function (div) { + var classes = div.className.split(/\s+/); + var lang = ''; + classes.forEach(function (cls) { + var match = cls.match(/^language-(.+)$/); + if (match) lang = match[1]; + }); + + if (!lang) return; + + var pre = div.querySelector('pre'); + var code = div.querySelector('code'); + if (pre && code) { + code.className = 'language-' + lang; + pre.className = 'language-' + lang; + } + }); + + // Handle plain markdown ```java blocks that Jekyll may render as + //
    
    +    document.querySelectorAll('pre code[class*="language-"]').forEach(function (code) {
    +      var pre = code.parentElement;
    +      if (pre && pre.tagName === 'PRE' && !pre.className.match(/language-/)) {
    +        var langClass = code.className.match(/language-\S+/);
    +        if (langClass) {
    +          pre.classList.add(langClass[0]);
    +        }
    +      }
    +    });
    +
    +    // Handle code without language class — treat as plain text
    +    document.querySelectorAll('pre code:not([class*="language-"])').forEach(function (code) {
    +      code.classList.add('language-none');
    +    });
    +  }
    +
    +  // Sync Prism stylesheet with current theme
    +  function syncPrismTheme() {
    +    var theme = document.documentElement.getAttribute('data-theme');
    +    var lightSheet = document.getElementById('prism-light');
    +    var darkSheet = document.getElementById('prism-dark');
    +
    +    if (lightSheet && darkSheet) {
    +      lightSheet.disabled = (theme === 'dark');
    +      darkSheet.disabled = (theme !== 'dark');
    +    }
    +  }
    +
    +  // Watch for theme changes (from theme-toggle.js)
    +  var observer = new MutationObserver(function (mutations) {
    +    mutations.forEach(function (mutation) {
    +      if (mutation.attributeName === 'data-theme') {
    +        syncPrismTheme();
    +      }
    +    });
    +  });
    +
    +  observer.observe(document.documentElement, {
    +    attributes: true,
    +    attributeFilter: ['data-theme']
    +  });
    +
    +  // Initialize
    +  function init() {
    +    bridgeRougeToprism();
    +    syncPrismTheme();
    +
    +    // Re-highlight with Prism
    +    if (window.Prism) {
    +      Prism.highlightAll();
    +    }
    +  }
    +
    +  if (document.readyState === 'loading') {
    +    document.addEventListener('DOMContentLoaded', init);
    +  } else {
    +    init();
    +  }
    +})();
    diff --git a/docs/assets/js/theme-toggle.js b/docs/assets/js/theme-toggle.js
    new file mode 100644
    index 0000000..ce67651
    --- /dev/null
    +++ b/docs/assets/js/theme-toggle.js
    @@ -0,0 +1,86 @@
    +// ==============================================
    +// Theme Toggle — Timer Ninja
    +// Day/Night mode with localStorage persistence
    +// ==============================================
    +
    +(function () {
    +  'use strict';
    +
    +  var STORAGE_KEY = 'timer-ninja-theme';
    +  var toggle = document.getElementById('themeToggle');
    +  var html = document.documentElement;
    +
    +  function getPreferredTheme() {
    +    var stored = localStorage.getItem(STORAGE_KEY);
    +    if (stored) return stored;
    +    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    +  }
    +
    +  function setTheme(theme) {
    +    html.setAttribute('data-theme', theme);
    +    localStorage.setItem(STORAGE_KEY, theme);
    +  }
    +
    +  // Initialize
    +  setTheme(getPreferredTheme());
    +
    +  // Toggle
    +  if (toggle) {
    +    toggle.addEventListener('click', function () {
    +      var current = html.getAttribute('data-theme');
    +      setTheme(current === 'dark' ? 'light' : 'dark');
    +    });
    +  }
    +
    +  // Listen for system preference changes
    +  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
    +    if (!localStorage.getItem(STORAGE_KEY)) {
    +      setTheme(e.matches ? 'dark' : 'light');
    +    }
    +  });
    +
    +  // Hamburger menu
    +  var hamburger = document.getElementById('navHamburger');
    +  var navLinks = document.getElementById('navLinks');
    +
    +  if (hamburger && navLinks) {
    +    hamburger.addEventListener('click', function () {
    +      hamburger.classList.toggle('is-open');
    +      navLinks.classList.toggle('is-open');
    +    });
    +
    +    // Close on link click (mobile)
    +    navLinks.querySelectorAll('.navbar__link').forEach(function (link) {
    +      link.addEventListener('click', function () {
    +        hamburger.classList.remove('is-open');
    +        navLinks.classList.remove('is-open');
    +      });
    +    });
    +  }
    +
    +  // Navbar background on scroll + back-to-top
    +  var navbar = document.getElementById('navbar');
    +  var backToTop = document.getElementById('backToTop');
    +
    +  if (navbar || backToTop) {
    +    window.addEventListener('scroll', function () {
    +      var scrolled = window.scrollY > 50;
    +      if (navbar) {
    +        navbar.style.boxShadow = scrolled ? '0 2px 20px rgba(0,0,0,0.08)' : 'none';
    +      }
    +      if (backToTop) {
    +        if (window.scrollY > 400) {
    +          backToTop.classList.add('is-visible');
    +        } else {
    +          backToTop.classList.remove('is-visible');
    +        }
    +      }
    +    }, { passive: true });
    +  }
    +
    +  if (backToTop) {
    +    backToTop.addEventListener('click', function () {
    +      window.scrollTo({ top: 0, behavior: 'smooth' });
    +    });
    +  }
    +})();
    diff --git a/docs/assets/js/typing-effect.js b/docs/assets/js/typing-effect.js
    new file mode 100644
    index 0000000..ac8dc28
    --- /dev/null
    +++ b/docs/assets/js/typing-effect.js
    @@ -0,0 +1,82 @@
    +// ==============================================
    +// Typing Effect — Timer Ninja
    +// Animated code typing for comparison section
    +// ==============================================
    +
    +(function () {
    +  'use strict';
    +
    +  var codeTraditional = document.getElementById('codeTraditional');
    +  var codeTimerNinja = document.getElementById('codeTimerNinja');
    +
    +  if (!codeTraditional || !codeTimerNinja) return;
    +
    +  function animateTyping(element, text, speed) {
    +    element.textContent = '';
    +    element.style.visibility = 'visible';
    +
    +    var i = 0;
    +    var cursor = document.createElement('span');
    +    cursor.className = 'typing-cursor';
    +    element.appendChild(cursor);
    +
    +    return new Promise(function (resolve) {
    +      function type() {
    +        if (i < text.length) {
    +          element.insertBefore(
    +            document.createTextNode(text.charAt(i)),
    +            cursor
    +          );
    +          i++;
    +          setTimeout(type, speed);
    +        } else {
    +          setTimeout(function () {
    +            if (cursor.parentNode) {
    +              cursor.parentNode.removeChild(cursor);
    +            }
    +            // Restore text and apply Prism highlighting
    +            element.textContent = text;
    +            if (window.Prism) {
    +              Prism.highlightElement(element);
    +            }
    +            resolve();
    +          }, 600);
    +        }
    +      }
    +      type();
    +    });
    +  }
    +
    +  // Store original text
    +  var traditionalText = codeTraditional.textContent;
    +  var ninjaText = codeTimerNinja.textContent;
    +
    +  // Only run animation once when section is visible
    +  var comparisonSection = document.getElementById('comparison');
    +  if (!comparisonSection) return;
    +
    +  var hasAnimated = false;
    +
    +  var observer = new IntersectionObserver(function (entries) {
    +    entries.forEach(function (entry) {
    +      if (entry.isIntersecting && !hasAnimated) {
    +        hasAnimated = true;
    +        observer.unobserve(entry.target);
    +
    +        // Start typing animation on both panels
    +        codeTraditional.textContent = '';
    +        codeTimerNinja.textContent = '';
    +
    +        setTimeout(function () {
    +          animateTyping(codeTraditional, traditionalText, 18).then(function () {
    +            return animateTyping(codeTimerNinja, ninjaText, 22);
    +          });
    +        }, 400);
    +      }
    +    });
    +  }, {
    +    threshold: 0.4
    +  });
    +
    +  observer.observe(comparisonSection);
    +})();
    diff --git a/docs/examples.md b/docs/examples.md
    new file mode 100644
    index 0000000..9503fe4
    --- /dev/null
    +++ b/docs/examples.md
    @@ -0,0 +1,321 @@
    +---
    +layout: docs
    +title: Examples
    +description: "Real-world examples demonstrating Timer Ninja usage patterns."
    +prev_page:
    +  title: User Guide
    +  url: /user-guide/
    +next_page:
    +  title: Advanced Usage
    +  url: /advanced-usage/
    +---
    +
    +# Examples
    +
    +This page provides real-world examples demonstrating Timer Ninja usage patterns.
    +
    +---
    +
    +## Basic Method Tracking
    +
    +### Simple Tracking
    +
    +```java
    +@TimerNinjaTracker
    +public void processRequest() {
    +    System.out.println("Processing request...");
    +}
    +```
    +
    +**Output:**
    +```
    +{===== Start of trace context id: abc123... =====}
    +public void processRequest() - 42 ms
    +{====== End of trace context id: abc123... ======}
    +```
    +
    +### With Time Unit
    +
    +```java
    +@TimerNinjaTracker(timeUnit = ChronoUnit.MICROS)
    +public void calculateMetrics() {
    +    // Precision calculation
    +}
    +```
    +
    +**Output:**
    +```
    +public void calculateMetrics() - 52341 µs
    +```
    +
    +---
    +
    +## Banking Service Example
    +
    +This example shows a comprehensive banking service with multiple tracking scenarios.
    +
    +### Money Transfer Service
    +
    +```java
    +public class BankService {
    +    private BalanceService balanceService;
    +    private UserService userService;
    +    private NotificationService notificationService;
    +
    +    public BankService() {
    +        BankRecordBook masterRecordBook = BankRecordBook.getInstance();
    +        this.notificationService = new NotificationService();
    +        this.balanceService = new BalanceService(masterRecordBook, notificationService);
    +        this.userService = new UserService(masterRecordBook);
    +    }
    +
    +    @TimerNinjaTracker(threshold = 200)
    +    public void requestMoneyTransfer(int sourceUserId, int targetUserId, int amount) {
    +        User sourceUser = userService.findUser(sourceUserId);
    +        User targetUser = userService.findUser(targetUserId);
    +        balanceService.deductAmount(sourceUser, amount);
    +        balanceService.increaseAmount(targetUser, amount);
    +    }
    +
    +    @TimerNinjaTracker(includeArgs = true, threshold = 500)
    +    public void depositMoney(int userId, int amount) {
    +        // Deposit logic
    +    }
    +
    +    @TimerNinjaTracker(includeArgs = true)
    +    public void payWithCard(int userId, BankCard card, int amount) {
    +        User user = userService.findUser(userId);
    +        // Card payment logic
    +    }
    +}
    +```
    +
    +### Output Example
    +
    +```
    +{===== Start of trace context id: 851ac23b-2669-4883-8c97-032b8fd2d45c =====}
    +public void requestMoneyTransfer(int sourceUserId, int targetUserId, int amount) - 1037 ms ¤ [Threshold Exceed !!: 200 ms]
    +   |-- public User findUser(int userId) - 105 ms
    +   |-- public User findUser(int userId) - 108 ms
    +   |-- public void deductAmount(User user, int amount) - 306 ms
    +   |-- public void increaseAmount(User user, int amount) - 418 ms
    +{====== End of trace context id: 851ac23b-2669-4883-8c97-032b8fd2d45c ======}
    +```
    +
    +---
    +
    +## Notification Service Example
    +
    +Demonstrates nested method tracking with multiple levels.
    +
    +```java
    +public class NotificationService {
    +
    +    @TimerNinjaTracker
    +    public void notify(User user) {
    +        notifyViaSMS(user);
    +        notifyViaEmail(user);
    +    }
    +
    +    @TimerNinjaTracker
    +    private void notifyViaSMS(User user) {
    +        try { Thread.sleep(50); }
    +        catch (InterruptedException e) { throw new RuntimeException(e); }
    +    }
    +
    +    @TimerNinjaTracker
    +    private void notifyViaEmail(User user) {
    +        try { Thread.sleep(200); }
    +        catch (InterruptedException e) { throw new RuntimeException(e); }
    +    }
    +}
    +```
    +
    +**Output:**
    +```
    +{===== Start of trace context id: abc123... =====}
    +public void notify(User user) - 258 ms
    +   |-- private void notifyViaSMS(User user) - 53 ms
    +   |-- private void notifyViaEmail(User user) - 205 ms
    +{====== End of trace context id: abc123... ======}
    +```
    +
    +---
    +
    +## Constructor Tracking
    +
    +### Service Initialization Chain
    +
    +```java
    +public class TransportationService {
    +    private ShippingService shippingService;
    +
    +    @TimerNinjaTracker
    +    public TransportationService() {
    +        this.shippingService = new ShippingService();
    +    }
    +}
    +
    +public class ShippingService {
    +    @TimerNinjaTracker
    +    public ShippingService() {
    +        // Shipping service initialization
    +    }
    +}
    +```
    +
    +**Output:**
    +```
    +{===== Start of trace context id: def456... =====}
    +public TransportationService() - 150 ms
    +   |-- public ShippingService() - 80 ms
    +{====== End of trace context id: def456... ======}
    +```
    +
    +---
    +
    +## Loan Processing Example
    +
    +Combines annotation-based tracking with block tracking.
    +
    +```java
    +public class LoanService {
    +    private UserService userService;
    +
    +    @TimerNinjaTracker(includeArgs = true, threshold = 100)
    +    public void processLoanApplication(int userId, double loanAmount, int termMonths) {
    +        User user = userService.findUser(userId);
    +
    +        // Phase 1: Credit check
    +        TimerNinjaBlock.measure("credit score check", () -> {
    +            simulateDelay(60);
    +        });
    +
    +        // Phase 2: Income verification
    +        TimerNinjaBlock.measure("income verification", () -> {
    +            simulateDelay(80);
    +        });
    +
    +        // Phase 3: Risk assessment with custom config
    +        BlockTrackerConfig riskConfig = new BlockTrackerConfig()
    +            .setTimeUnit(ChronoUnit.MILLIS)
    +            .setThreshold(30);
    +
    +        TimerNinjaBlock.measure("risk assessment", riskConfig, () -> {
    +            simulateDelay(40);
    +        });
    +
    +        // Phase 4: Final approval with return value
    +        String approvalStatus = TimerNinjaBlock.measure("final approval", () -> {
    +            simulateDelay(50);
    +            return "APPROVED";
    +        });
    +    }
    +}
    +```
    +
    +**Output:**
    +```
    +{===== Start of trace context id: ghi789... =====}
    +public void processLoanApplication(int userId, double loanAmount, int termMonths) - Args: [userId={123}, loanAmount={50000.0}, termMonths={36}] - 345 ms
    +   |-- [Block] credit score check - 60 ms
    +   |-- [Block] income verification - 80 ms
    +   |-- [Block] risk assessment - 40 ms
    +   |-- [Block] final approval - 50 ms
    +{====== End of trace context id: ghi789... ======}
    +```
    +
    +---
    +
    +## E-commerce Order Processing
    +
    +```java
    +@Service
    +public class OrderService {
    +
    +    @TimerNinjaTracker
    +    public Order createOrder(OrderRequest request) {
    +        Order order = validateAndCreateOrder(request);
    +        PaymentResult paymentResult = processPayment(order);
    +        updateInventory(order);
    +        sendConfirmation(order);
    +        return order;
    +    }
    +
    +    @TimerNinjaTracker(threshold = 500, includeArgs = true)
    +    private PaymentResult processPayment(Order order) {
    +        return paymentService.charge(
    +            order.getUserId(), order.getPaymentMethod(), order.getTotalAmount()
    +        );
    +    }
    +
    +    @TimerNinjaTracker(threshold = 200)
    +    private void updateInventory(Order order) {
    +        order.getItems().forEach(item ->
    +            inventoryService.deductStock(item.getProductId(), item.getQuantity())
    +        );
    +    }
    +
    +    @TimerNinjaTracker
    +    private void sendConfirmation(Order order) {
    +        notificationService.sendEmailConfirmation(order.getUserEmail(), order);
    +    }
    +}
    +```
    +
    +**Output:**
    +```
    +{===== Start of trace context id: jkl012... =====}
    +public Order createOrder(OrderRequest request) - 2150 ms
    +   |-- public Order validateAndCreateOrder(OrderRequest request) - 120 ms
    +   |-- public PaymentResult processPayment(Order order) - Args: [order={id=ORD-12345, ...}] - 1250 ms ¤ [Threshold Exceed !!: 500 ms]
    +      |-- public PaymentResult charge(int userId, String paymentMethod, double amount) - 1180 ms
    +   |-- public void updateInventory(Order order) - 450 ms
    +   |-- public void sendConfirmation(Order order) - 330 ms
    +{====== End of trace context id: jkl012... ======}
    +```
    +
    +---
    +
    +## API Controller Example
    +
    +```java
    +@RestController
    +@RequestMapping("/api/users")
    +public class UserController {
    +
    +    @TimerNinjaTracker
    +    @GetMapping("/{id}")
    +    public ResponseEntity getUser(@PathVariable Long id) {
    +        User user = userService.findById(id);
    +        return ResponseEntity.ok(user);
    +    }
    +
    +    @TimerNinjaTracker(includeArgs = true, threshold = 100)
    +    @PostMapping
    +    public ResponseEntity createUser(@RequestBody CreateUserRequest request) {
    +        User user = userService.create(request);
    +        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    +    }
    +}
    +```
    +
    +**Output for GET request:**
    +```
    +{===== Start of trace context id: mno345... =====}
    +public ResponseEntity getUser(Long id) - 85 ms
    +   |-- public User findById(Long id) - 70 ms
    +      |-- public User queryDatabase(Long id) - 65 ms
    +{====== End of trace context id: mno345... ======}
    +```
    +
    +---
    +
    +## Key Takeaways
    +
    +1. **Entry Point Tracking** — Track high-level methods to capture full call hierarchies
    +2. **Threshold Usage** — Use thresholds to filter noise and focus on slow operations
    +3. **Argument Tracking** — Enable `includeArgs` for debugging and performance analysis
    +4. **Block Tracking** — Use `TimerNinjaBlock` for granular tracking without method extraction
    +5. **Constructor Tracking** — Track initialization chains to identify slow startup times
    +6. **Mixed Tracking** — Combine annotation and block tracking for comprehensive monitoring
    diff --git a/docs/index.html b/docs/index.html
    new file mode 100644
    index 0000000..72d98bd
    --- /dev/null
    +++ b/docs/index.html
    @@ -0,0 +1,317 @@
    +---
    +layout: home
    +title: Home
    +description: "Timer Ninja — A sneaky library for Java Method Timing. Track execution time with a single annotation. Zero boilerplate."
    +---
    +
    +
    +
    + +
    +
    + Timer Ninja mascot — a ninja sloth +
    +
    + Open Source · Java Library +
    +

    + A Sneaky Library for
    Java Method Timing +

    +

    + Track execution time with a single annotation. Preserve call hierarchies. Zero boilerplate. Built on AspectJ. +

    + +
    + Latest + io.github.thanglequoc:timer-ninja:1.3.0 +
    +
    + +
    + + +
    +
    +
    + Why Timer Ninja? +

    Performance Tracking,
    Without the Pain

    +

    Everything you need to understand your code's timing behavior — nothing you don't.

    +
    +
    +
    +
    🎯
    +

    One Annotation

    +

    Replace 6 lines of timestamp boilerplate with @TimerNinjaTracker. That's it.

    +
    +
    +
    🌳
    +

    Visual Call Tree

    +

    Nested method calls render as a clear, indented hierarchy. See the full execution flow at a glance.

    +
    +
    +
    🧩
    +

    Block Tracking

    +

    Measure arbitrary code blocks with TimerNinjaBlock.measure() — no need to extract separate methods.

    +
    +
    +
    ⚠️
    +

    Smart Thresholds

    +

    Only surface slow operations. Set a threshold and Timer Ninja filters the noise automatically.

    +
    +
    +
    🪶
    +

    Zero Dependencies

    +

    Just AspectJ + SLF4J. No framework lock-in. Works with Spring Boot, plain Java, or anything in between.

    +
    +
    +
    🛡️
    +

    Thread-Safe

    +

    Isolated per-thread context via ThreadLocal. Safe for concurrent and multi-threaded applications.

    +
    +
    +
    +
    + + +
    +
    +
    + Before & After +

    Stop Timing Methods
    The Hard Way

    +

    See how Timer Ninja eliminates manual timestamp boilerplate.

    +
    + +
    +
    +
    + Traditional Approach +
    +
    +
    long before = System.currentTimeMillis();
    +doSomethingInteresting();
    +long after = System.currentTimeMillis();
    +System.out.println(
    +  "Execution time (ms): " + (after - before)
    +);
    +
    +
    + +
    +
    + ✔️ Timer Ninja +
    +
    +
    @TimerNinjaTracker
    +public String doSomethingInteresting() {
    +    // Your business logic — that's it!
    +}
    +
    +
    +
    + +
    +
    + 6 lines of boilerplate + + 1 annotation +
    +
    +
    +
    + + +
    +
    +
    + See It In Action +

    Beautiful Trace Output

    +

    Timer Ninja prints a visual call tree showing the full execution hierarchy, timing, and arguments.

    +
    + +
    +
    + + + + Timer Ninja Trace Output +
    +
    +Timer Ninja trace context id: 851ac23b-2669-4883-8c97-032b8fd2d45c +Trace timestamp: 2023-04-03T07:16:48.491Z +{===== Start of trace context id: 851ac23b... =====} +public void requestMoneyTransfer(...) - Args: [sourceUserId={1}, targetUserId={2}, amount={500}] - 1747 ms + |-- public User findUser(int userId) - 105000 µs + |-- public void processPayment(User user, int amount) - 770 ms + |-- public boolean changeAmount(User user, int amount) - 306 ms + |-- public void notify(User user) - 258 ms + |-- private void notifyViaSMS(User user) - 53 ms + |-- private void notifyViaEmail(User user) - 205 ms ¤ [Threshold Exceed !!: 200 ms] +{====== End of trace context id: 851ac23b... ======} +
    +
    +
    +
    + + +
    +
    +
    + Quick Start +

    Get Started in 60 Seconds

    +

    Four simple steps to start tracking method execution time.

    +
    + +
    + +
    +
    1
    +
    +

    Add the Dependency

    +

    Add Timer Ninja from Maven Central to your project.

    +
    + + +
    +
    +
    implementation 'io.github.thanglequoc:timer-ninja:1.3.0'
    +aspect 'io.github.thanglequoc:timer-ninja:1.3.0'
    +
    +
    +
    <dependency>
    +    <groupId>io.github.thanglequoc</groupId>
    +    <artifactId>timer-ninja</artifactId>
    +    <version>1.3.0</version>
    +</dependency>
    +
    +
    +
    + + +
    +
    2
    +
    +

    Add AspectJ Plugin

    +

    Enable AspectJ compilation so annotations get woven.

    +
    + + +
    +
    +
    plugins {
    +    id "io.freefair.aspectj.post-compile-weaving" version '9.1.0'
    +}
    +
    +
    +
    <plugin>
    +    <groupId>dev.aspectj</groupId>
    +    <artifactId>aspectj-maven-plugin</artifactId>
    +    <version>1.14.1</version>
    +    <configuration>
    +        <aspectLibraries>
    +            <aspectLibrary>
    +                <groupId>io.github.thanglequoc</groupId>
    +                <artifactId>timer-ninja</artifactId>
    +            </aspectLibrary>
    +        </aspectLibraries>
    +    </configuration>
    +</plugin>
    +
    +
    +
    + + +
    +
    3
    +
    +

    Annotate Your Methods

    +

    Place @TimerNinjaTracker on any method or constructor.

    +
    @TimerNinjaTracker
    +public void processPayment(User user, int amount) {
    +    // Your business logic
    +}
    +
    +
    + + +
    +
    4
    +
    +

    Run & See the Trace 🐇

    +

    Execute your code — Timer Ninja automatically logs the execution trace.

    +
    public void processPayment(User user, int amount) - 770 ms
    +   |-- public boolean changeAmount(User user, int amount) - 306 ms
    +   |-- public void notify(User user) - 258 ms
    +
    +
    +
    +
    +
    + + +
    +
    +
    + Advanced Feature +

    Block Tracking with
    TimerNinjaBlock

    +

    Measure any code block — no need to extract separate methods.

    +
    + +
    +
    +
    + 🧩 Block Tracking Example +
    +
    +
    @TimerNinjaTracker(includeArgs = true, threshold = 100)
    +public void processLoanApplication(int userId, double amount, int months) {
    +    User user = userService.findUser(userId);
    +
    +    TimerNinjaBlock.measure("credit score check", () -> {
    +        simulateDelay(60);
    +    });
    +
    +    TimerNinjaBlock.measure("income verification", () -> {
    +        simulateDelay(80);
    +    });
    +
    +    String status = TimerNinjaBlock.measure("final approval", () -> {
    +        simulateDelay(50);
    +        return "APPROVED";
    +    });
    +}
    +
    +
    +
    +
    +
    + + +
    +
    +
    + Timer Ninja mascot +
    +

    Ready to Track Like a Ninja?

    +

    + Start measuring method execution time the smart way. Zero boilerplate, full visibility. +

    + +
    +
    diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..b3506e0 --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,420 @@ +--- +layout: docs +title: User Guide +description: "Comprehensive guide on how to use Timer Ninja's features effectively." +prev_page: + title: Home + url: / +next_page: + title: Examples + url: /examples/ +--- + +# User Guide + +This guide provides detailed documentation on how to use Timer Ninja's features effectively. + +--- + +## Annotation-based Tracking + +The `@TimerNinjaTracker` annotation is the primary way to track method execution time. + +### Basic Usage + +Annotate any method or constructor to start tracking: + +```java +@TimerNinjaTracker +public void performTask() { + // Your business logic +} +``` + +### Tracking Constructors + +You can also track constructor execution: + +```java +@TimerNinjaTracker +public class NotificationService { + public NotificationService() { + // Constructor logic + } +} +``` + +**Output:** +``` +{===== Start of trace context id: abc123... =====} +public NotificationService() - 80 ms +{====== End of trace context id: abc123... ======} +``` + +### Annotation Attributes + +The `@TimerNinjaTracker` annotation supports several configuration options: + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `enabled` | `boolean` | `true` | Enable or disable tracking for this method | +| `timeUnit` | `ChronoUnit` | `MILLIS` | Time unit for measurement (SECONDS, MILLIS, MICROS) | +| `includeArgs` | `boolean` | `false` | Include method arguments in the log trace | +| `threshold` | `int` | `-1` | Minimum execution time required to log (in specified timeUnit) | + +--- + +## Configuration Options + +### 1. Enable/Disable Tracking + +Control whether a specific method is tracked: + +```java +@TimerNinjaTracker(enabled = true) +public void trackThis() { + // This will be tracked +} + +@TimerNinjaTracker(enabled = false) +public void dontTrackThis() { + // This will NOT be tracked +} +``` + +**Use Case:** Temporarily disable tracking for a method without removing the annotation. + +### 2. Time Unit Selection + +Choose the appropriate time unit for your measurement needs: + +```java +import java.time.temporal.ChronoUnit; + +@TimerNinjaTracker(timeUnit = ChronoUnit.SECONDS) +public void longRunningOperation() { + // For operations taking seconds +} + +@TimerNinjaTracker(timeUnit = ChronoUnit.MILLIS) +public void standardOperation() { + // For operations taking milliseconds (default) +} + +@TimerNinjaTracker(timeUnit = ChronoUnit.MICROS) +public void preciseOperation() { + // For operations requiring microsecond precision +} +``` + +**Supported Units:** +- `ChronoUnit.SECONDS` — Seconds +- `ChronoUnit.MILLIS` — Milliseconds (default) +- `ChronoUnit.MICROS` — Microseconds + +### 3. Include Method Arguments + +Log method arguments for better debugging context: + +```java +@TimerNinjaTracker(includeArgs = true) +public void processUser(int userId, String name, String email) { + // Method logic +} +``` + +**Output:** +``` +public void processUser(int userId, String name, String email) - Args: [userId={123}, name={John Doe}, email={john@example.com}] - 42 ms +``` + +> **Important:** Ensure your objects have proper `toString()` implementations for meaningful output. + +### 4. Threshold Filtering + +Filter out fast methods to focus on performance issues: + +```java +@TimerNinjaTracker(threshold = 500) // Only log if execution > 500ms +public void potentiallySlowMethod() { + // Method logic +} +``` + +**When Threshold is Exceeded:** +``` +public void potentiallySlowMethod() - 723 ms ¤ [Threshold Exceed !!: 500 ms] +``` + +**When Below Threshold:** The method is suppressed from the trace output. If all methods in a trace are below threshold, a summary is shown. + +**Combining with Arguments:** +```java +@TimerNinjaTracker(includeArgs = true, threshold = 200) +public void requestMoneyTransfer(int sourceUserId, int targetUserId, int amount) { + // Only logs slow transfers with full argument details +} +``` + +--- + +## Block Tracking + +For granular tracking within a method without extracting separate methods, use `TimerNinjaBlock`. + +### Basic Block Tracking + +```java +public void processData() { + TimerNinjaBlock.measure("database query", () -> { + database.query("SELECT * FROM users"); + }); +} +``` + +### Block with Return Value + +```java +public void processData() { + String result = TimerNinjaBlock.measure("fetch data", () -> { + return api.fetchUserData(); + }); + System.out.println(result); +} +``` + +### Block with Custom Configuration + +```java +import java.time.temporal.ChronoUnit; + +public void processData() { + BlockTrackerConfig config = new BlockTrackerConfig() + .setTimeUnit(ChronoUnit.SECONDS) + .setThreshold(2); + + TimerNinjaBlock.measure("long operation", config, () -> { + performLongRunningTask(); + }); +} +``` + +### Nested Block Tracking + +```java +public void complexProcess() { + TimerNinjaBlock.measure("overall process", () -> { + loadData(); + TimerNinjaBlock.measure("data transformation", () -> { + transformData(); + }); + saveData(); + }); +} +``` + +**Output:** +``` +{===== Start of trace context id: ... =====} +[Block] overall process - 1500 ms + |-- [Block] data transformation - 500 ms +{====== End of trace context id: ... ======} +``` + +--- + +## Understanding Trace Output + +### Trace Structure + +``` +Timer Ninja trace context id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +Trace timestamp: 2023-04-03T14:27:50.322Z +{===== Start of trace context id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 =====} +public void parentMethod() - 100 ms + |-- public void childMethod() - 50 ms + |-- public void anotherChildMethod() - 30 ms +{====== End of trace context id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 ======} +``` + +### Elements Explained + +1. **Trace Context ID** — Auto-generated UUID for a trace. All tracked methods in the same call stack share this ID. +2. **Trace Timestamp** — When the trace context was initiated (UTC timezone). +3. **Start/End Markers** — Delimit the trace boundaries. +4. **Method Lines** — Each tracked method shows: signature, arguments (if enabled), execution time, threshold indicator. +5. **Indentation (`|--`)** — Shows call hierarchy. Indented methods are called by the method above. + +### Summary Output + +When all methods in a trace are below their thresholds: + +``` +Timer Ninja trace context id: abc123... +Trace timestamp: 2023-04-03T14:27:50.322Z +All 3 tracked items within threshold. min: 5 ms, max: 45 ms, total: 50 ms +``` + +--- + +## Installation + +### Add the Timer Ninja Dependency + +**Gradle:** +```groovy +implementation group: 'io.github.thanglequoc', name: 'timer-ninja', version: '1.3.0' +``` + +**Maven:** +```xml + + io.github.thanglequoc + timer-ninja + 1.3.0 + compile + +``` + +### Declare AspectJ Plugin + +**Gradle** — using [FreeFair AspectJ Gradle plugin](https://github.com/freefair/gradle-plugins): + +```groovy +plugins { + id "io.freefair.aspectj.post-compile-weaving" version '9.1.0' +} + +dependencies { + implementation group: 'io.github.thanglequoc', name: 'timer-ninja', version: '1.3.0' + aspect 'io.github.thanglequoc:timer-ninja:1.3.0' + + // Enable this if you want to track methods in test classes + testAspect("io.github.thanglequoc:timer-ninja:1.3.0") +} +``` + +**Maven** — using [Forked Mojo's AspectJ Plugin](https://github.com/dev-aspectj/aspectj-maven-plugin): + +```xml + + dev.aspectj + aspectj-maven-plugin + 1.14.1 + + + org.aspectj + aspectjtools + 1.9.25 + + + + ${java.version} + + + io.github.thanglequoc + timer-ninja + + + + + + + compile + test-compile + + + + +``` + +--- + +## Global Configuration + +### Enable System.out Logging + +For simple console applications or quick testing: + +```java +TimerNinjaConfiguration.getInstance().toggleSystemOutLog(true); +``` + +> Call this once at application startup. By default, Timer Ninja uses SLF4J logging. + +### Log Level + +The logger class is `io.github.thanglequoc.timerninja.TimerNinjaUtil` with default level `INFO`. + +To enable debug information: + +```xml + +``` + +--- + +## Best Practices + +### Choose Appropriate Time Units + +- **Seconds** — For long-running operations (API calls, file I/O, batch processing) +- **Milliseconds** — For general application logic (default) +- **Microseconds** — For performance-critical code (algorithms, calculations) + +### Use Thresholds Strategically + +```java +// Too low - creates noise +@TimerNinjaTracker(threshold = 10) + +// Too high - miss issues +@TimerNinjaTracker(threshold = 5000) + +// Balanced - catches real issues +@TimerNinjaTracker(threshold = 200) +``` + +### Track Entry Points + +Add tracking to high-level entry points (REST controllers, main methods) to capture full execution traces: + +```java +@RestController +public class UserController { + @TimerNinjaTracker + @GetMapping("/users/{id}") + public User getUser(@PathVariable Long id) { + return userService.findById(id); + } +} +``` + +### Don't Track Everything + +**Focus on:** +- Critical business logic +- External API calls +- Database operations +- File I/O operations + +**Avoid tracking:** +- Simple getters/setters +- Very fast operations (< 1ms) +- Trivial utility methods + +--- + +## Troubleshooting + +### No Output in Logs + +1. Check if SLF4J provider is configured +2. Verify log level is at least `INFO` +3. Enable `System.out` for testing: `TimerNinjaConfiguration.getInstance().toggleSystemOutLog(true);` + +### Methods Not Being Tracked + +1. Verify AspectJ plugin is configured correctly +2. Check that the dependency includes the aspect: `aspect 'io.github.thanglequoc:timer-ninja:1.3.0'` +3. Ensure `enabled = true` (or not set) on the annotation