当前位置: 首页 > news >正文

PipeWire 音频设计与实现分析三——日志子系统

日志子系统

PipeWire 的日志子系统的设计分为多个层次。PipeWire 用 struct spa_log 对象描述日志组件,用 struct spa_log_methods 对象描述日志组件打印各层级日志的多个方法。PipeWire 为日志子系统添加了 topic 机制,不同文件中的日志按功能以不同的 topic 来打印,以方便针对 topic 的日志分类处理。PipeWire 用 struct spa_log_topic 对象描述日志 topic。日志组件的接口名为SPA_TYPE_INTERFACE_Log Spa:Pointer:Interface:Log。这些结构定义 (位于 pipewire/spa/include/spa/support/log.h) 如下:

#define SPA_LOG_TOPIC_DEFAULT NULL

enum spa_log_level {
	SPA_LOG_LEVEL_NONE = 0,
	SPA_LOG_LEVEL_ERROR,
	SPA_LOG_LEVEL_WARN,
	SPA_LOG_LEVEL_INFO,
	SPA_LOG_LEVEL_DEBUG,
	SPA_LOG_LEVEL_TRACE,
};

/**
 * The Log interface
 */
#define SPA_TYPE_INTERFACE_Log	SPA_TYPE_INFO_INTERFACE_BASE "Log"


struct spa_log {
	/** the version of this log. This can be used to expand this
	 * structure in the future */
#define SPA_VERSION_LOG		0
	struct spa_interface iface;
	/**
	 * Logging level, everything above this level is not logged
	 */
	enum spa_log_level level;
};

/**
 * \struct spa_log_topic
 *
 * Identifier for a topic. Topics are string-based filters that logically
 * group messages together. An implementation may decide to filter different
 * topics on different levels, for example the "protocol" topic may require
 * debug level TRACE while the "core" topic defaults to debug level INFO.
 *
 * spa_log_topics require a spa_log_methods version of 1 or higher.
 */
struct spa_log_topic {
#define SPA_VERSION_LOG_TOPIC	0
	/** the version of this topic. This can be used to expand this
	 * structure in the future */
	uint32_t version;
	/** The string identifier for the topic */
	const char *topic;
	/** Logging level set for this topic */
	enum spa_log_level level;
	/** False if this topic follows the \ref spa_log level */
	bool has_custom_level;
};

struct spa_log_methods {
#define SPA_VERSION_LOG_METHODS		1
	uint32_t version;
	/**
	 * Log a message with the given log level.
	 *
	 * \note If compiled with this header, this function is only called
	 * for implementations of version 0. For versions 1 and above, see
	 * logt() instead.
	 *
	 * \param log a spa_log
	 * \param level a spa_log_level
	 * \param file the file name
	 * \param line the line number
	 * \param func the function name
	 * \param fmt printf style format
	 * \param ... format arguments
	 */
	void (*log) (void *object,
		     enum spa_log_level level,
		     const char *file,
		     int line,
		     const char *func,
		     const char *fmt, ...) SPA_PRINTF_FUNC(6, 7);

	/**
	 * Log a message with the given log level.
	 *
	 * \note If compiled with this header, this function is only called
	 * for implementations of version 0. For versions 1 and above, see
	 * logtv() instead.
	 *
	 * \param log a spa_log
	 * \param level a spa_log_level
	 * \param file the file name
	 * \param line the line number
	 * \param func the function name
	 * \param fmt printf style format
	 * \param args format arguments
	 */
	void (*logv) (void *object,
		      enum spa_log_level level,
		      const char *file,
		      int line,
		      const char *func,
		      const char *fmt,
		      va_list args) SPA_PRINTF_FUNC(6, 0);
	/**
	 * Log a message with the given log level for the given topic.
	 *
	 * \note Callers that do not use topic-based logging (version 0), the \a
	 * topic is NULL
	 *
	 * \param log a spa_log
	 * \param level a spa_log_level
	 * \param topic the topic for this message, may be NULL
	 * \param file the file name
	 * \param line the line number
	 * \param func the function name
	 * \param fmt printf style format
	 * \param ... format arguments
	 *
	 * \since 1
	 */
	void (*logt) (void *object,
		     enum spa_log_level level,
		     const struct spa_log_topic *topic,
		     const char *file,
		     int line,
		     const char *func,
		     const char *fmt, ...) SPA_PRINTF_FUNC(7, 8);

	/**
	 * Log a message with the given log level for the given topic.
	 *
	 * \note For callers that do not use topic-based logging (version 0),
	 * the \a topic is NULL
	 *
	 * \param log a spa_log
	 * \param level a spa_log_level
	 * \param topic the topic for this message, may be NULL
	 * \param file the file name
	 * \param line the line number
	 * \param func the function name
	 * \param fmt printf style format
	 * \param args format arguments
	 *
	 * \since 1
	 */
	void (*logtv) (void *object,
		      enum spa_log_level level,
		      const struct spa_log_topic *topic,
		      const char *file,
		      int line,
		      const char *func,
		      const char *fmt,
		      va_list args) SPA_PRINTF_FUNC(7, 0);

	/**
	 * Initializes a \ref spa_log_topic to the correct logging level.
	 *
	 * \since 1
	 */
	void (*topic_init) (void *object, struct spa_log_topic *topic);
};

PipeWire 的 SPA 日志系统提供了许多宏,来以不同的日志等级和 topic,向 spa_log 打印日志,这些宏定义 (位于 pipewire/spa/include/spa/support/log.h) 如下:

/* Unused, left for backwards compat */
#define spa_log_level_enabled(l,lev) ((l) && (l)->level >= (lev))

#define spa_log_level_topic_enabled(l,topic,lev)		\
({								\
	struct spa_log *_log = l;				\
	enum spa_log_level _lev = _log ? _log->level : SPA_LOG_LEVEL_NONE;		\
	struct spa_log_topic *_t = (struct spa_log_topic *)topic; \
	if (_t && _t->has_custom_level)							\
		_lev = _t->level;				\
	_lev >= lev;						\
})

/* Transparently calls to version 0 log if v1 is not supported */
#define spa_log_logt(l,lev,topic,...)					\
({									\
	struct spa_log *_l = l;						\
	struct spa_interface *_if = &_l->iface;				\
	if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \
		if (!spa_interface_call(_if,				\
				struct spa_log_methods, logt, 1,	\
				lev, topic,				\
				__VA_ARGS__))				\
		    spa_interface_call(_if,				\
				struct spa_log_methods, log, 0,		\
				lev, __VA_ARGS__);			\
	}								\
})

/* Transparently calls to version 0 logv if v1 is not supported */
#define spa_log_logtv(l,lev,topic,...)					\
({									\
	struct spa_log *_l = l;						\
	struct spa_interface *_if = &_l->iface;				\
	if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \
		if (!spa_interface_call(_if,				\
				struct spa_log_methods, logtv, 1,	\
				lev, topic,				\
				__VA_ARGS__))				\
		    spa_interface_call(_if,				\
				struct spa_log_methods, logv, 0,	\
				lev, __VA_ARGS__);			\
	}								\
})

#define spa_log_log(l,lev,...)					\
	spa_log_logt(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)

#define spa_log_logv(l,lev,...)					\
	spa_log_logtv(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)

#define spa_log_error(l,...)	spa_log_log(l,SPA_LOG_LEVEL_ERROR,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_log_warn(l,...)	spa_log_log(l,SPA_LOG_LEVEL_WARN,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_log_info(l,...)	spa_log_log(l,SPA_LOG_LEVEL_INFO,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_log_debug(l,...)	spa_log_log(l,SPA_LOG_LEVEL_DEBUG,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_log_trace(l,...)	spa_log_log(l,SPA_LOG_LEVEL_TRACE,__FILE__,__LINE__,__func__,__VA_ARGS__)

#define spa_logt_error(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_ERROR,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_logt_warn(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_WARN,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_logt_info(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_INFO,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_logt_debug(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_DEBUG,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
#define spa_logt_trace(l,t,...)	spa_log_logt(l,SPA_LOG_LEVEL_TRACE,t,__FILE__,__LINE__,__func__,__VA_ARGS__)

#ifndef FASTPATH
#define spa_log_trace_fp(l,...)	spa_log_log(l,SPA_LOG_LEVEL_TRACE,__FILE__,__LINE__,__func__,__VA_ARGS__)
#else
#define spa_log_trace_fp(l,...)
#endif

这些宏最终通过 struct spa_log_methods 的方法打印日志。

PipeWire 的代码通常通过另一组日志宏 (也称为 pipewire 日志宏) 打印日志,如 pw_log_info()pw_log_warn(),这组宏定义 (位于 pipewire/src/pipewire/log.h) 如下:

/** Log a message for a topic */
void
pw_log_logt(enum spa_log_level level,
	    const struct spa_log_topic *topic,
	    const char *file,
	    int line, const char *func,
	    const char *fmt, ...) SPA_PRINTF_FUNC(6, 7);

/** Log a message for a topic */
void
pw_log_logtv(enum spa_log_level level,
	     const struct spa_log_topic *topic,
	     const char *file,
	     int line, const char *func,
	     const char *fmt, va_list args) SPA_PRINTF_FUNC(6, 0);



/** Log a message for the default topic */
void
pw_log_log(enum spa_log_level level,
	   const char *file,
	   int line, const char *func,
	   const char *fmt, ...) SPA_PRINTF_FUNC(5, 6);

/** Log a message for the default topic */
void
pw_log_logv(enum spa_log_level level,
	    const char *file,
	    int line, const char *func,
	    const char *fmt, va_list args) SPA_PRINTF_FUNC(5, 0);
 . . . . . .
/** Check if a loglevel is enabled */
#define pw_log_level_enabled(lev) (pw_log_level >= (lev))
#define pw_log_topic_enabled(lev,t) ((t) && (t)->has_custom_level ? (t)->level >= (lev) : pw_log_level_enabled((lev)))

#define pw_logtv(lev,topic,fmt,ap)						\
({										\
	if (SPA_UNLIKELY(pw_log_topic_enabled(lev,topic)))			\
		pw_log_logtv(lev,topic,__FILE__,__LINE__,__func__,fmt,ap);	\
})

#define pw_logt(lev,topic,...)							\
({										\
	if (SPA_UNLIKELY(pw_log_topic_enabled(lev,topic)))			\
		pw_log_logt(lev,topic,__FILE__,__LINE__,__func__,__VA_ARGS__);	\
})

#define pw_log(lev,...) pw_logt(lev,PW_LOG_TOPIC_DEFAULT,__VA_ARGS__)

#define pw_log_error(...)   pw_log(SPA_LOG_LEVEL_ERROR,__VA_ARGS__)
#define pw_log_warn(...)    pw_log(SPA_LOG_LEVEL_WARN,__VA_ARGS__)
#define pw_log_info(...)    pw_log(SPA_LOG_LEVEL_INFO,__VA_ARGS__)
#define pw_log_debug(...)   pw_log(SPA_LOG_LEVEL_DEBUG,__VA_ARGS__)
#define pw_log_trace(...)   pw_log(SPA_LOG_LEVEL_TRACE,__VA_ARGS__)

#define pw_logt_error(t,...)   pw_logt(SPA_LOG_LEVEL_ERROR,t,__VA_ARGS__)
#define pw_logt_warn(t,...)    pw_logt(SPA_LOG_LEVEL_WARN,t,__VA_ARGS__)
#define pw_logt_info(t,...)    pw_logt(SPA_LOG_LEVEL_INFO,t,__VA_ARGS__)
#define pw_logt_debug(t,...)   pw_logt(SPA_LOG_LEVEL_DEBUG,t,__VA_ARGS__)
#define pw_logt_trace(t,...)   pw_logt(SPA_LOG_LEVEL_TRACE,t,__VA_ARGS__)

#ifndef FASTPATH
#define pw_log_trace_fp(...)   pw_log(SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
#else
#define pw_log_trace_fp(...)
#endif

这组宏是对一组日志函数的封装。这组日志函数定义 (位于 pipewire/src/pipewire/log.c) 如下:

SPA_LOG_IMPL(default_log);

#define DEFAULT_LOG_LEVEL SPA_LOG_LEVEL_WARN

SPA_EXPORT
enum spa_log_level pw_log_level = DEFAULT_LOG_LEVEL;

static struct spa_log *global_log = &default_log.log;
 . . . . . .
/** Log a message for the given topic
 * \param level the log level
 * \param topic the topic
 * \param file the file this message originated from
 * \param line the line number
 * \param func the function
 * \param fmt the printf style format
 * \param ... printf style arguments to log
 *
 */
SPA_EXPORT
void
pw_log_logt(enum spa_log_level level,
	    const struct spa_log_topic *topic,
	    const char *file,
	    int line,
	    const char *func,
	    const char *fmt, ...)
{
	if (SPA_UNLIKELY(pw_log_topic_enabled(level, topic))) {
		va_list args;
		va_start(args, fmt);
		spa_log_logtv(global_log, level, topic, file, line, func, fmt, args);
		va_end(args);
	}
}

/** Log a message for the given topic with va_list
 * \param level the log level
 * \param topic the topic
 * \param file the file this message originated from
 * \param line the line number
 * \param func the function
 * \param fmt the printf style format
 * \param args a va_list of arguments
 *
 */
SPA_EXPORT
void
pw_log_logtv(enum spa_log_level level,
	     const struct spa_log_topic *topic,
	     const char *file,
	     int line,
	     const char *func,
	     const char *fmt,
	     va_list args)
{
	spa_log_logtv(global_log, level, topic, file, line, func, fmt, args);
}


/** Log a message for the default topic with va_list
 * \param level the log level
 * \param file the file this message originated from
 * \param line the line number
 * \param func the function
 * \param fmt the printf style format
 * \param args a va_list of arguments
 *
 */
SPA_EXPORT
void
pw_log_logv(enum spa_log_level level,
	    const char *file,
	    int line,
	    const char *func,
	    const char *fmt,
	    va_list args)
{
	pw_log_logtv(level, PW_LOG_TOPIC_DEFAULT, file, line, func, fmt, args);
}

/** Log a message for the default topic
 * \param level the log level
 * \param file the file this message originated from
 * \param line the line number
 * \param func the function
 * \param fmt the printf style format
 * \param ... printf style arguments to log
 *
 */
SPA_EXPORT
void
pw_log_log(enum spa_log_level level,
	   const char *file,
	   int line,
	   const char *func,
	   const char *fmt, ...)
{
	va_list args;
	va_start(args, fmt);
	pw_log_logtv(level, PW_LOG_TOPIC_DEFAULT, file, line, func, fmt, args);
	va_end(args);
}

这组日志函数,主要封装了对 SPA 日志宏的调用。当然在调用 SPA 日志宏之前,会先检查要打印的日志的日志等级,按照日志等级设置是否需要打印。此外,日志组件实现 spa_log 取全局日志组件。

PipeWire 代码通过 pipewire 日志宏或 pipewire 日志函数打印日志。

PipeWire 的日志组件实现有多个,默认的日志组件实现 (位于 pipewire/spa/include/spa/support/log-impl.h) 如下:

static inline SPA_PRINTF_FUNC(7, 0) void spa_log_impl_logtv(void *object,
				     enum spa_log_level level,
				     const struct spa_log_topic *topic,
				     const char *file,
				     int line,
				     const char *func,
				     const char *fmt,
				     va_list args)
{
	static const char * const levels[] = { "-", "E", "W", "I", "D", "T" };

	const char *basename = strrchr(file, '/');
	char text[512], location[1024];
	char topicstr[32] = {0};

	if (basename)
		basename += 1; /* skip '/' */
	else
		basename = file; /* use whole string if no '/' is found */

	if (topic && topic->topic)
		snprintf(topicstr, sizeof(topicstr), " %s ", topic->topic);
	vsnprintf(text, sizeof(text), fmt, args);
	snprintf(location, sizeof(location), "[%s]%s[%s:%i %s()] %s\n",
		 levels[level],
		 topicstr,
		 basename, line, func, text);
	fputs(location, stderr);
}

static inline SPA_PRINTF_FUNC(7,8) void spa_log_impl_logt(void *object,
				    enum spa_log_level level,
				    const struct spa_log_topic *topic,
				    const char *file,
				    int line,
				    const char *func,
				    const char *fmt, ...)
{
	va_list args;
	va_start(args, fmt);
	spa_log_impl_logtv(object, level, topic, file, line, func, fmt, args);
	va_end(args);
}

static inline SPA_PRINTF_FUNC(6, 0) void spa_log_impl_logv(void *object,
				     enum spa_log_level level,
				     const char *file,
				     int line,
				     const char *func,
				     const char *fmt,
				     va_list args)
{

	spa_log_impl_logtv(object, level, NULL, file, line, func, fmt, args);
}

static inline SPA_PRINTF_FUNC(6,7) void spa_log_impl_log(void *object,
				    enum spa_log_level level,
				    const char *file,
				    int line,
				    const char *func,
				    const char *fmt, ...)
{
	va_list args;
	va_start(args, fmt);
	spa_log_impl_logv(object, level, file, line, func, fmt, args);
	va_end(args);
}

static inline void spa_log_impl_topic_init(void *object, struct spa_log_topic *topic)
{
	/* noop */
}

#define SPA_LOG_IMPL_DEFINE(name)		\
struct {					\
	struct spa_log log;			\
	struct spa_log_methods methods;		\
} name

#define SPA_LOG_IMPL_INIT(name)				\
	{ { { SPA_TYPE_INTERFACE_Log, SPA_VERSION_LOG,	\
	      SPA_CALLBACKS_INIT(&name.methods, &name) },	\
	    SPA_LOG_LEVEL_INFO,	},			\
	  { SPA_VERSION_LOG_METHODS,			\
	    spa_log_impl_log,				\
	    spa_log_impl_logv,				\
	    spa_log_impl_logt,				\
	    spa_log_impl_logtv,				\
	  } }

#define SPA_LOG_IMPL(name)			\
        SPA_LOG_IMPL_DEFINE(name) = SPA_LOG_IMPL_INIT(name)

日志组件实现定义 struct spa_log_methods 对象及其各个方法,以及 struct spa_log 对象。日志组件的使用者通过 struct spa_log 对象的 iface.cb.funcs 字段获得它实现的 struct spa_log_methods 对象,获得它提供的各个打印日志的方法。

这个默认实现将日志打印到标准输出。日志组件实现更常见的是从支持库插件或 journal 插件加载。

PipeWire 通过一个全局对象维护默认使用的日志组件实现,并提供一些接口来操作 (位于 pipewire/src/pipewire/log.c) 该对象,如:

SPA_EXPORT
void pw_log_set(struct spa_log *log)
{
	global_log = log ? log : &default_log.log;
	global_log->level = pw_log_level;
}

bool pw_log_is_default(void)
{
	return global_log == &default_log.log;
}

/** Get the global log interface
 * \return the global log
 */
SPA_EXPORT
struct spa_log *pw_log_get(void)
{
	return global_log;
}

/** Set the global log level
 * \param level the new log level
 */
SPA_EXPORT
void pw_log_set_level(enum spa_log_level level)
{
	pw_log_level = level;
	global_log->level = level;
}

PipeWire 还提供接口初始化 (位于 pipewire/src/pipewire/log.c) 日志 topic,如:

SPA_EXPORT
struct spa_log_topic *PW_LOG_TOPIC_DEFAULT;

PW_LOG_TOPIC_STATIC(log_topic, "pw.log"); /* log topic for this file here */
PW_LOG_TOPIC(log_buffers, "pw.buffers");
PW_LOG_TOPIC(log_client, "pw.client");
PW_LOG_TOPIC(log_conf, "pw.conf");
PW_LOG_TOPIC(log_context, "pw.context");
PW_LOG_TOPIC(log_core, "pw.core");
PW_LOG_TOPIC(log_data_loop, "pw.data-loop");
PW_LOG_TOPIC(log_device, "pw.device");
PW_LOG_TOPIC(log_factory, "pw.factory");
PW_LOG_TOPIC(log_filter, "pw.filter");
PW_LOG_TOPIC(log_global, "pw.global");
PW_LOG_TOPIC(log_link, "pw.link");
PW_LOG_TOPIC(log_loop, "pw.loop");
PW_LOG_TOPIC(log_main_loop, "pw.main-loop");
PW_LOG_TOPIC(log_mem, "pw.mem");
PW_LOG_TOPIC(log_metadata, "pw.metadata");
PW_LOG_TOPIC(log_module, "pw.module");
PW_LOG_TOPIC(log_node, "pw.node");
PW_LOG_TOPIC(log_port, "pw.port");
PW_LOG_TOPIC(log_properties, "pw.props");
PW_LOG_TOPIC(log_protocol, "pw.protocol");
PW_LOG_TOPIC(log_proxy, "pw.proxy");
PW_LOG_TOPIC(log_resource, "pw.resource");
PW_LOG_TOPIC(log_stream, "pw.stream");
PW_LOG_TOPIC(log_thread_loop, "pw.thread-loop");
PW_LOG_TOPIC(log_work_queue, "pw.work-queue");

PW_LOG_TOPIC(PW_LOG_TOPIC_DEFAULT, "default");
 . . . . . .
 void
pw_log_init(void)
{
	PW_LOG_TOPIC_INIT(PW_LOG_TOPIC_DEFAULT);
	PW_LOG_TOPIC_INIT(log_buffers);
	PW_LOG_TOPIC_INIT(log_client);
	PW_LOG_TOPIC_INIT(log_conf);
	PW_LOG_TOPIC_INIT(log_context);
	PW_LOG_TOPIC_INIT(log_core);
	PW_LOG_TOPIC_INIT(log_data_loop);
	PW_LOG_TOPIC_INIT(log_device);
	PW_LOG_TOPIC_INIT(log_factory);
	PW_LOG_TOPIC_INIT(log_filter);
	PW_LOG_TOPIC_INIT(log_global);
	PW_LOG_TOPIC_INIT(log_link);
	PW_LOG_TOPIC_INIT(log_loop);
	PW_LOG_TOPIC_INIT(log_main_loop);
	PW_LOG_TOPIC_INIT(log_mem);
	PW_LOG_TOPIC_INIT(log_metadata);
	PW_LOG_TOPIC_INIT(log_module);
	PW_LOG_TOPIC_INIT(log_node);
	PW_LOG_TOPIC_INIT(log_port);
	PW_LOG_TOPIC_INIT(log_properties);
	PW_LOG_TOPIC_INIT(log_protocol);
	PW_LOG_TOPIC_INIT(log_proxy);
	PW_LOG_TOPIC_INIT(log_resource);
	PW_LOG_TOPIC_INIT(log_stream);
	PW_LOG_TOPIC_INIT(log_thread_loop);
	PW_LOG_TOPIC_INIT(log_topic);
	PW_LOG_TOPIC_INIT(log_work_queue);
}

各个需要打印日志的源文件,定义 PW_LOG_TOPIC_DEFAULT 宏选择所要用的日志 topic,如:

PW_LOG_TOPIC_EXTERN(log_context);
#define PW_LOG_TOPIC_DEFAULT log_context

则 PipeWire 打印日志的方法,分为如下四层:

  1. PipeWire 日志打印宏。许多代码会直接用它们打印日志。封装 PipeWire 日志打印函数。
  2. PipeWire 日志打印函数。部分代码会直接用它们打印日志。封装 SPA 日志打印宏,通过全局日志打印组件打印日志。
  3. SPA 日志打印宏。插件代码会直接用它们打印日志。通过日志打印组件实现打印日志。
  4. 日志打印组件实现。

全局日志打印组件管理:通过一个全局对象维护,提供接口来设置和获取。

日志打印组件实现:有一个内置的实现,但大多通过插件实现,如支持库插件和 journal 插件。

http://www.dtcms.com/a/99866.html

相关文章:

  • vxe-table 设置单元格可编辑无效问题解决
  • 网络传输优化之多路复用与解复用
  • 流动的梦境:GPT-4o 的自回归图像生成深度解析
  • 聚焦应用常用功能,提升用户体验与分发效率
  • 桥接模式_结构型_GOF23
  • day17 学习笔记
  • Gateway实战入门(四)、断言-请求头以及请求权重分流等
  • Kafka 多线程开发消费者实例
  • 第四天 文件操作(文本/CSV/JSON) - 异常处理机制 - 练习:日志文件分析器
  • 【Python】基于 qwen_agent 构建 AI 绘画智能助手
  • Linux 文件存储和删除原理
  • Unity编辑器功能及拓展(2) —Gizmos编辑器绘制功能
  • Kafka消息丢失全解析!原因、预防与解决方案
  • 如何查看 SQL Server 的兼容性级别
  • 基于ruoyi快速开发平台搭建----超市仓库管理(修改记录1)
  • 《C++11:通过thread类编写C++多线程程序》
  • 编辑器场景视窗扩展
  • SpringBean模块(一)定义如何创建生命周期
  • 《C++Linux编程进阶:从0实现muduo 》-第6讲.C++死锁问题如何分析调试-原子操作,互斥量,条件变量的封装
  • 稻壳模板下载器(Windows):免费获取WPS稻壳模板的利器
  • Java中优先级队列的实现
  • 蓝桥杯备考------>双指针(滑动窗口)
  • 华为OD机试2025A卷 - 最大值(Java Python JS C++ C )
  • PyTorch 2.6.0没有对应的torch-sparse版本,不要下载pytorch最新版本,否则用不了图神经网络torch_geometric
  • vmware_unbantu刷新IP
  • EtherNet/IP转ProfiNet协议转换网关驱动西门子PLC与流量计的毫秒级压力同步控制
  • 洛谷题单1-P5706 【深基2.例8】再分肥宅水-python-流程图重构
  • MySQL单表查询、多表查询
  • linux实现rsync+sersync实时数据备份
  • Redis-快速入门